Hacker News new | past | comments | ask | show | jobs | submit login
WebAssembly and Back Again: Fine-Grained Sandboxing in Firefox 95 (hacks.mozilla.org)
392 points by feross on Dec 6, 2021 | hide | past | favorite | 144 comments



WebAssembly is kind of a hack here (although a clever hack that saves a lot of effort) - the essence of what the Mozilla folks have done isn't WebAssembly, it's a trusted compiler - by which I mean a compiler that emits trustable code, regardless of how untrusted the source is. It's a really neat idea that I hope to see more adoption of, because our current security models for software suck.

Security based on process isolation is extremely inefficient and coarse-grained - having a trusted compiler could (eventually) massively increase performance by removing processes entirely (no more virtual memory! no more TLB flushes and misses! less task switch overhead!) and eliminating the kernel/user mode separation, with an increase in security.

"Could" because it's not clear to me if the reduction in expressiveness from our languages now to future languages with a theoretical trusted compiler (all jump targets have to be known at compile-time?) will be accepted by the majority of the populace. Look at how hard it is to get people to accept borrow-checkers...


> having a trusted compiler could (eventually) massively increase performance by removing processes entirely (no more virtual memory! no more TLB flushes and misses! less task switch overhead!) and eliminating the kernel/user mode separation

I saw a talk a while ago that was advocating for the same thing, except this was about JS and not webassembly. I can't find it tho - I remember it being related to the WAT js talk; It also mentioned that it would eliminate rings on the cpu (and simplify cpus) and context switches which would make execution faster; they were citing some MS research on the matter - damn I really wanna find the talk now...

Edit: https://www.destroyallsoftware.com/talks/the-birth-and-death...

thanks BoppreH

MS research: "Hardware-based isolation incurs nontrivial performance costs (up to 25-33%) and complicates system implementations" (virtual memory and protection rings); I think MS knows what they're talking about here


Singularity was a experimental OS written in a a variant of C# and .Net managed code by MS Research that ran using software isolated processes rather than hardware isolation, this is probably what they where referencing:

https://en.wikipedia.org/wiki/Singularity_(operating_system)


http://joeduffyblog.com/2015/11/03/blogging-about-midori/

There is also a really great blog about Singularity’s “rebirth” experimental OS, Midori, that continued in its footsteps.


Thanks for the link. I would argue that a true trusted compiler needs to accept an unmanaged language and emit code without a runtime, though. A runtime is cheating, because you can always make one that implements an iron-clad sandbox that doesn't require processes...by implementing a (very slow) VM.

To put in another way - I don't think that security or performance are that hard to achieve on their own - the hard part is getting both at once. And then, adding expressiveness on top is even more difficult, as Rust as aptly demonstrated.


Rust is not secure at all in the sense used here — untrusted, arbitrary user code written in rust is a security threat.


More specifically, unsafe blocks may violate the compiler's security guarantees and procedural macros actually run inside the compiler process at build time. Declarative macros do this too, but they're far too restricted to allow shenanigans. Procmacros can disable Rust's stability guarantees[0].

[0] https://github.com/m-ou-se/nightly-crimes


Nah, that’s not what I mean. It is a Turing complete language — if it is used to interpret some other language inside itself, it can’t add anything to that languages’ guarantees automatically. You can write a javascript interpreter in rust that is trivial to exploit and access e.g. the file system or whatever.


I think I heard of this in the early to mid 00’s and it was in the context of Java. This set of ideas has been cooking for a while. Might be about time to taste the proverbial soup.


> it's a trusted compiler

Sorry I have to quibble here, but this term is already a thing, and typically has the opposite connotation: a compiler that must be trusted because we cannot verify the output. It's trusted because we "trust" it (to not screw up).

I would argue that this makes the compiler untrusted--we don't care what it does, whatever it outputs is going to be both statically and dynamically verified to not break the sandbox properties.

> Security based on process isolation is extremely inefficient and coarse-grained - having a trusted compiler could (eventually) massively increase performance by removing processes entirely (no more virtual memory! no more TLB flushes and misses! less task switch overhead!) and eliminating the kernel/user mode separation, with an increase in security.

I thought this right up until, well, about this time in 2017. Side-channel attacks are a real and bad thing. Our conclusion is that Spectre, in all its flavors, break confidentiality for in-process memory, regardless of sandboxing technology. On current hardware, there is no 100% bulletproof way to enforce isolation.


Well yeah isn't the same thing then ? The resulting code is plain x86 assembly, and you just trust it to have been sanitized by its transformation to/from WASM. Looks like your definition of a trusted compiler to me.


"Trustable" compiler, perhaps?


Trustworthy Compiler is the term we're looking for (analogous to trustworthy computing vs. trusted computing aka JANUS/Intel ME/etc)


> a compiler that emits trustable code, regardless of how untrusted the source is

Hasn't this been one of the goals of the design of WebAssembly since day one, and something that has been getting people excited about it too? Using something for one of its intended purposes isn't really a "hack", no?


Sort of. The main goal for WebAssembly is to be a fast platform-agnostic compilation target that you can use in websites. The "in websites" bit means that it has to be completely safe (i.e. no accessing outside memory etc.) but it's not the main goal.

This is a bit of a hack because Mozilla don't care about the platform-agnostic bit, so they're taking LLVM IR, compiling it to WebAssembly, then back to LLVM IR just so that they can ensure that the code is safe.

WebAssembly comes with extra constraints that you probably don't care about if you're compiling to native (e.g. there's no 'goto') so you would get more efficient code (and probably faster compilation) if you just had some way of compiling LLVM IR directly to "safe binary".

That would probably be a mountain of work though so its understandable why they went with this. Would be nice if they said how much the performance was impacted.


The hack is translating wasm back to C, then compiling again. It wouldn’t be a hack if they’re running wasm, but they’re not.


AFAIK the ability to compile wasm to native code was pretty much always part of the goal, running wasm in a VM was never the end-game.

Compiling via C might be considered a hack (especially in the sense that it introduces a potential weak link in the chain), but it makes a lot of sense since they want to integrate the result into the Firefox build artefacts, and compiling via C is not exactly novel either.


In 2021, the world finally achieves AS/400 on the web.


Can you expand on that? Was this a property of C compilers or other languages on IBM mainframes?

I get that it's tongue in cheek, but it would probably be even funnier / ironic if I had more context to understand it.

It's the same spirit as languages adopting functional programming techniques aka rediscovering Lisp.


AS/400 had a machine-independent binary format which was translated ahead of execution by the system's specific compiler into machine code, and all applications ran in the same address space with zero memory protection because the code generated by the compiler ensured isolation.


Incredible, I always thought that was an idea worth exploring. AOT compilation and not paying the cost of context and MMU switch would unlock incredible performance boosts, even on modern processors. I didn't know AS/400 did that already.

IIRC Microsoft explored something similar with their research OS Singularity.



>because the code generated by the compiler ensured isolation

That's fascinating. How does it do so? I thought it only separated the address space into user space and OS space.


I imagine that this was possible because the compiler had help from the AS400 architecture. In other words, doing this for a complex architecture like i64 would be much more difficult.


Modern AS/400 runs on PowerPC, the hardware is completly different from the early 1980's models.


I understand the ISA abstraction. That’s pretty common.

I’m interested in how the compiler ensures process isolation.


No need for past tense, AS/400 still exists, just got the name changed to IBM i.


Oh wow. So again, we have come a full circle?


Yeah, it sounds like this is a clever hack to get clang to compile C code into a safe binary form; with runtime validation around pointers and array lookups and things.

I'm surprised clang doesn't simply have a mode to do this already. Its weird we need this strange wasm dance. I'm imagining a sort of a release mode version of ASAN + UBSAN where the compiled code has bounds checks for all array and pointer lookups. (And probably some more checks, besides). The rule is that it should be impossible for any binary code compiled this way to overrun arrays, or read or write memory outside of its allocated section. No matter the C code, the binary is memory-safe.

It would have a (modest) cost in runtime performance and binary size, but frankly that would be a trade off well worth making in all sorts of software we use daily.


> I'm surprised clang doesn't simply have a mode to do this already. Its weird we need this strange wasm dance

Compiling to WebAssembly doesn't make the code safe; it simply compiles it to a virtual machine with a memory model that isolates any bugs from the rest of the parent process (presuming a bug-free parent process).

Basically, all pointers in WebAssembly are indexes to a global array (or global arrays--one for data and another for functions). It's basically the same trick people use in Rust to get around restrictions on reference cycles. And like in Rust, the compiled code can still have the same dangling pointer and buffer overflow problems, but the memory thus referenced is restricted to the unstructured array, rather than to potentially any address in the parent process' address space.

The difference between Rust and WASM is that when you compile WASM to native code, at least when JIT'ing the code, you can completely elide the extra degree of memory reference indirection. (I don't think the extra indirection can be elided for AOT compilation of WASM code without a special runtime linking step.) The cost, however, is that you're left with this single global data array, so any bug in the WASM program can break any other part of the WASM program, whereas in Rust you effectively end up with many segments of unstructured memory (basically, one per component that uses the trick) that can't be cross-referenced if the program is buggy.


There's a very good reason such a thing doesn't exist: any C compiler that worked like that would break the vast majority of C programs.

Fundamentally, memory safety requires that you keep track of not just where a pointer currently points to, but where a pointer could validly point to. But, as applied in practice, the C ABI requires that pointers be integers of where it currently points to; making pointers also include range information (aka "fat" pointers) is possible, but requires rewriting almost any software you care about--it's the kind of change that's about as fundamental to C programs as assuming 2's complement arithmetic.

The way tools like ASAN and Valgrind work is by instead keeping track of where any pointer could point to (via shadow maps, basically a bit per memory location saying if you can read/write that location), and then padding every allocation with sufficient space that an innocuous read overflow (especially one-past-the-end) is likely to hit the padding. Their guarantees are at best probabilistic, not anything you would want to rely on for security.


Most C and C++ code is perfectly happy with fat pointers provided you take care in how you implement them, especially around (u)intptr_t. For CHERI we see a very tiny % of LoC that need changing; e.g. http://www.capabilitieslimited.co.uk/pdfs/20210917-capltd-ch... documents a recent case study in porting a KDE desktop stack (X11 itself, Qt, other standard graphical desktop libraries, Plasma, Dolphin, Okular) and across the 6 million LoC only 0.026% needed changing.

So, if you pick your fat pointer implementation poorly, then yes, you run up against both the standard and de-facto C, but if you take care then the vast majority of C code just works, and we're (uniquely?) positioned to be able to prove that by having a FreeBSD-based kernel and userspace, and graphical desktop stack (plus an adaptation of WebKit's JSC JIT) all built with a compiler that maps C pointers to capabilities that enforce fine-grained bounds.


Given that one can compile C to Wasm it is simply not true that it would break C code. Wasm implements pointers as offset into an array and then checks for out-of-bounds access. LLVM should be able to do that directly.


> runtime validation around pointers and array lookups and things.

I think you're right but "validation around pointers and array lookups" makes it sound far better than it is. That sounds like the sort of strong validation is "safe" Rust gives you. But all it does is ensure the WASM virtual machine "executing" the WASM code doesn't access RAM outside of the VM's memory space. Everything in that memory space is fair game, so all the foot shooting stuff C allows like corrupting pointers and following them within that memory space is still possible.

It achieves what they want for security I guess, but not much more.


We will always need process boundaries to separate information domains, because side-channel vulnerabilities are not addressed by having a "trusted" compile step.


When it comes to side-channel attacks, process boundaries aren't adequate either. Rowhammer, Meltdown/Spectre, and friends show that handily, and the RSA key leakage attack using SDR shows that even machine isolation isn't going to be enough for some things.

I guess that the idea that trusted compilers are the way forward is predicated on the assumption that we've managed to mitigate most/all side-channel attacks, because there really isn't much you can do about those otherwise.


Those side channels existed because because they very clearly violated process isolation. Of course processes aren't going to isolate if your hardware ignores your isolation boundaries.

Enforcing this kind of isolation in software will likely be quite difficult though.


Why can't a trusted compiler prevent side-channel attacks? All you need to do is prevent the code from accessing the side channel. It seems to me that doing this at compile time would actually be easier than doing it at run time.


It is easy. You just have to forbid access to timers and loops. Where "timers" include anything that can count and store the count in shared memory.

Alternatively, you could forbid branches (and therefore loops, implicitly).


I presume that was meant to be facetious? But a clock is not the thing you would prevent access to. The thing you would prevent access to is whatever it is you want to try to time using that clock.


But if you forbid those things, what's left? Loops and branches are what code is made of.


If you can use a loop for timing then you can use an unrolled loop for timing too.


A key side-channel is execution time, and no, in general, you can't prevent a program from getting a clock. Even without a clock, one can construct one easily using shared memory and threads. Clocks are also easy to find. Even with low resolution clocks, timing differences can be amplified programmatically, making them observable.


The clock is not the thing you would prevent access to. The thing you would prevent access to is whatever it is you want to try to time using that clock.


This sandboxing is achieved via RLBox[0], which is a toolkit for sandboxing third-party libraries. It comprises of a WASM sandbox and an API which existing application can leverage. The research paper[1].

[0]: https://plsyssec.github.io/rlbox_sandboxing_api/sphinx/

[1]: https://arxiv.org/abs/2003.00572


So if I'm understanding this correctly, Firefox compiles its dependencies as WASM, effectively blocking function calls to things it shouldn't and illegal memory access, and then translates it back to C so it can compile it normally? Sounds neat!


Not back into C, from WASM to native executable / asm / object code


Deukhoofd is correct — we compile the WASM code back into C in order to reuse and reduce friction with our existing compilation pipeline.



The solution described in the post actually translates C/C++ to WASM, and then translates the WASM bytecode back to C (via a tool called wasm2c), which is then fed back into the C compiler again to compile to native code, all 'offline' in the Firefox build process.


That was only for the prototype, the current implementation is:

source code -> WASM -> C -> native code


Getting strong vibes of The Birth and Death of JavaScript (2014) [1], one of the numerous great talks by Gary Bernhardt.

My engineer side is happy seeing how strong tooling enables such creative features with high assurances.

My futurist side is dreading the day Intel launches their first Javascript/WebAssembly-only processor.

[1] https://www.destroyallsoftware.com/talks/the-birth-and-death...


Brilliant, hilarious talk, thanks for linking.


i don't think JavaScript going to die but its time that we have another option for the web. JavaScript have its warts. some people love JavaScript and some don't. its not fair for JavaScript to be the only option. i see Web Assembly as an option for people who doesn't like JavaScript warts to use their favor langue to develop for the web.


I feel like the only people that "like" javascript or those that had it for their first language. Its needed, its better than it was, but compared to just about any other language its a total mess.


My career was C, Java, PHP, JavaScript.

I like JS the most.

It's flexible, lightweight, and omnipresent.

The only other mainstream language that gives me that feeling is Rust.


The ECMA improvements are nice but imo it's too hard to manage large Javascript programs, or Typescript wouldn't have such a large following.


Good point.

I'm using TypeScript and JavaScript interchangably.

I know JavaScript like the back of my hand, so TypeScript isn't telling me much new, but the static type checking allows me to keep less of that knowledge in working memory. Pretty awesome language!


Rust is hardly omnipresent. I understand it has trouble with lesser used architectures and operating systems. While yes you probably dont use them, they do still exist.


ARM already has Java and JavaScript extensions in their CPUs, so that day isn't completely off the horizon yet.

I'm not even sure it would be a terrible idea, as we'd have a very interesting JS/WASM-like set of opcodes that we could target with _any_ compiler.


The "Javascript instruction" is a bit of a misnomer.

JS accidentally got part of the x86 execution model for float conversion baked into the spec. ARM added an instruction to mimic the old x86 one. It's potentially useful in some other contexts too.


Regardless, FJCVTZS is still literally a "Javascript" instruction: "Floating-point Javascript Convert to Signed fixed-point, rounding toward Zero".


Firefox appears to utilize a custom clang toolchain to enable this without documenting how to make such toolchain (wasi sysroot). And expects you to just download the precompiled version from their servers.

Fedora and Fennec F-Droid have since disabled this feature.

https://src.fedoraproject.org/rpms/firefox/c/4cb1381d80a94c9...

https://gitlab.com/relan/fennecbuild/-/commit/12cdb51bb045c3...


Pretty sure you can build it yourself from https://github.com/WebAssembly/wasi-libc given that https://github.com/WebAssembly/wasi-libc/commit/ad5133410f66... is a contribution from a MoCo employee doing a lot of work around toolchains.


There's also the https://github.com/WebAssembly/wasi-sdk repo which is kind of a meta-build-system for all this.

But in FreeBSD we build all the pieces directly, here's our build recipes (with some hacks due to llvm's cmake code being stupid sometimes):

compiler-rt (from llvm): https://github.com/freebsd/freebsd-ports/blob/main/devel/was...

libc (from what you linked): https://github.com/freebsd/freebsd-ports/blob/main/devel/was...

libc++ (from llvm): https://github.com/freebsd/freebsd-ports/blob/main/devel/was...


You're responsible for making Firefox work on FreeBSD? Thank you! I really appreciate your work and I use it every day.


I'm not part of gecko@ but I wrote the devel/wasi-* ports and I've contributed to Firefox itself on a few occasions (touchpad scrolling on GTK and partial invalidation for EGL) :)


Looks like Arch Linux is building it themselves with --with-wasi-sysroot. The changes they made to the build script for the 95.0 release are pretty instructive: https://github.com/archlinux/svntogit-packages/commit/532ac4...

Hopefully Fedora manage to implement this to their satisfaction in the near future, although requiring extremely recent releases of build tools might be a blocker for some distros.


This is a really powerful tool and I hope we see this used more. Traditional process based sandboxing is very efficient inside the process, but IPC is very expensive. This approach flips the tradeoffs exactly backwards as the sandboxed code is slower, but IPC is nearly free. This means that it can cover exactly the space that was too expensive to sandbox before. The two approaches are perfect compliments for each other. I now imagine that the vast majority of code can be put into one of these two groups leaving very little code that is unable to be sandboxed for performance reasons.


Here is an example of sandboxing a library and then calling functions:

    rlbox::rlbox_sandbox<rlbox_noop_sandbox> sandbox;
    sandbox.create_sandbox();
    sandbox.invoke_sandbox_function(hello);
https://github.com/PLSysSec/rlbox_sandboxing_api/blob/master...

Seems like it could get a bit verbose when used all over the place but I guess there’s always a cost with security and having clearly defined risky parts also helps. Regardless I’m happy to see the effort being made beyond process isolation and OS capabilities.


The downside to this technique is that wasm2c code is 50% slower, so (at least for now) process-isolation is still a win in some cases (when the overhead of process-isolation is small compared to the rest).

Still, that's a very exciting development that could lead to a revolution in operating systems.


42% slower only in the worst case using the slowest (explicit) bounds checks, only 12% slower with a signal handler:

https://kripken.github.io/blog/wasm/2020/07/27/wasmboxc.html


I don't have a lot of knowledge in this area, but using WASM for forcing code to be safe seems bizarre. Why aren't there just compiler flags that can enforce the same restrictions they want?


Beyond the reasons others have mentioned, another key issue is that this isn't a transparent transformation. The sandboxed code can only access memory within a restricted subregion, which often requires some small code changes on both sides of the boundary (for example, copying input data into that memory region so that sandboxed code can operate on it).

So implementing this in the compiler would entail some fairly involved handshaking between the code and the compiler beyond the normal scope of C/C++. Doing this in a library instead — and leaning on a well-understood and well-studied execution model — makes everything a bit more natural to work with.


For technical reasons adding compiler flags to do that is fairly hard. You'd need to handle a lot of things like compiling to the sandboxed format, system library support, the FFI to normal code, etc. It would be possible to do all that, but wasm has already done it - so compiling to wasm as an intermediary step is the most practical solution.

(See also https://news.ycombinator.com/item?id=29460766)


Why is it bizarre? The Wasm function interface seems perfect for sandboxing code. It's a "whitelist" system, the contained process can only call external functions that have been explicitly attached, perfect for implementing the capability security paradigm and progressively hollowing out the attack surface by separating functionality into several instances.


Requiring compilation to WASM to then translate it back to C and compile it again might be a bit strange. Clang obviously already has the tools to do the WASM sanitizing, it might be really cool to have a way to directly enforce those rules outside of WASM.


As I understand it, clang doesn't have the tools to sanitize WASM. It just emits WASM, which, malicious or benevolent, can't access memory outside its designated memory regions.

It's wasm2c job to ensure that the C generated enforces the WASM memory rules, so I'd say the one sanitizing the code is not clang but wasm2c.


Webassembly is much more restricted than regular machine code.

It's a stack machine with a limited set of operations, no direct control over the stack/control flow and restricted access to memory.

It's way easier to compile this limited set of operations to assembly (or C) that is guaranteed to not do things it shouldn't.


> Why aren't there just compiler flags that can enforce the same restrictions they want?

Because they want to compile arbitrary code in order to sandbox it.

The alternative is something like eBPF, but that imposes a limited subset of the source language, which would be unlikely to work with something like a video decoder.


NaCL (native client) sort of did this, but through an entirely separate toolchain. It's not an easy task.


You definitely could do that. It would just be a ton of work and nobody has done it.


It is doable, but it's hard to make it fast on all platform. See the SegmentZero32 description in <https://cseweb.ucsd.edu/~dstefan/pubs/kolosick:2022:isolatio...> for an example prototype.


This looks more like using the compilation to wasm and back to automatically rewrite the code of entire components in a manner that makes them safer.


With the recompilation back to C I fail to see how RLbox prevents memory corruption due to lack of bounds checking or UB being explored by the optimizer.


There is that risk, yes. For wasm2c to be correct it must emit C code without undefined behavior. As best we know it does that properly today, and we've tested and fuzzed it quite a lot, but whenever you use an optimizing C compiler on the output there is no 100% guarantee.


The host application is still C, and the final compilation of the mix of C and sanitized C still relies on the optimizer to be fast, so it might be possible for the untrusted library to reveal UB in the rest of the application. I can see how the whole approach (wasm + taint) would make compromise of the sanitized bits through crafted data harder, but I'm not sure it does enough for guarding against a supply chain compromise of the library.


And I see the safe languages section[1] in the WasmBoxC post; that looks like it would address most of the remaining UB risk, but Firefox will take some time getting there.

[1]: https://kripken.github.io/blog/wasm/2020/07/27/wasmboxc.html...


There's no guarantee even without optimization. Some optimizations exploit UB, but not all "unexpected" code generation in the presence of UB is due to optimization.

Take signed int overflow on addition. On some platforms ADD wraps, on others it traps. Depending on target CPU you'll get very different behavior even for non-optimizing compilers that just emit an ADD instruction!

WASM is just another target, with its own behavior.


WASM is always bounds checked and doesn't have UB. The C code produced by wasm2c should be the same.


WASM doesn't do bounds checking inside linear memory segments, good luck preventing corruption on data structures stored on the same segment.

Unless the produced C code is validated against optimizations across every single compiler version, there are no guarantees of that actually being the case.


> WASM doesn't do bounds checking inside linear memory segments, good luck preventing corruption on data structures stored on the same segment.

Isn't the point that you don't care?

Each untrusted library is compiled to wasm then C then native, they can corrupt their own datastructures but the point is to prevent that corruption from escaping those boundaries, or at least that's how I understand it.


Mostly that's the case, yes. The main benefit of wasm sandboxing in this situation is to keep any exploit of these libraries in the sandbox - no memory corruption outside. That's a big improvement on running the same code outside of a sandbox.

(But in general corruption inside the sandbox is potentially dangerous too. You need to be careful about what you do with data you get from the sandboxed code. RLBox does help in that area as well.)


Just because corruption doesn't escape the sandbox doesn't means it isn't exploitable.

This like attacking microservices, you have a module that exposes a set of interfaces and produces outputs when called with specific APIs.

For the sake of example lets say you have an authentication module that says if a given user id is root.

Now imagine producing a sequence of API calls that it will trigger the side effect of is_root(id) being true for an id that it is a plain user.

No sandbox escape took place, only internal corruption of internal data keeping structures that lead the is_root() to misbehave.


The code implementing is_root is not an untrusted module.


Think of some online banking application written in C++ and compiled to the web. With js, they only have to worry about css exploits and similar. If they use C++, suddenly they also have to worry about all the C++ bugs, too.


they specifically address this in the article; the objective is to be able to treat the component as untrusted code (the same as a website's js) and sanitize/check the component's output (the same as web APIs called by js).

Corruption is not an issue when you assume the corruptee to already be an attacker


Programs running natively on a computer have some security measures in place that wasm binaries lack, while wasm has other security measures in place that native programs lack. Quoting the "Everything Old is New Again: Binary Security of WebAssembly" paper:

> Comparing the exploitability of WebAssembly binaries with native binaries, e.g., on x86, shows that WebAssembly re-enables several formerly defeated attacks because it lacks modern mitigations. One example are stack-based buffer overflows, which are effective again because WebAssembly binaries do not deploy stack canaries. Moreover, we find attacks not possible in this form in native binaries, such as overwriting string literals in supposedly constant memory.

https://news.ycombinator.com/item?id=24216764

https://www.unibw.de/patch/papers/usenixsecurity20-wasm.pdf


Yes but the impact of a wasm buffer overflow is completely different to a C buffer overflow. A C buffer overflow can (sometimes) let you execute arbitrary code. A wasm one will just result in data corruption.

That data corruption may be exploitable (e.g. if it lets you overwrite a password) but those kinds of attacks are much much rarer than the kind that wasm prevents.


Being rarer doesn't make them nonexistent, as the WebAssembly advocacy tends to be around most places.


This blog post explicitly calls out that out:

> the programmer only needs to sanitize any values that come from the sandbox (since they could be maliciously-crafted), a task which RLBox makes easy with a tainting layer.

I don't think I've ever heard any WebAssembly advocates saying that it magically solves all security issues.


What's the performance overhead in comparison to the unsandboxed version I wonder?


It varies. Here's an example of some performance analysis I did on the expat port: https://bugzilla.mozilla.org/show_bug.cgi?id=1688452#c37


There are detailed performance numbers here on a variety of real-world codebases:

https://kripken.github.io/blog/wasm/2020/07/27/wasmboxc.html

tl;dr Something like ~14% when using the best bounds checking strategy, or ~42% when using the most portable one. (There are options in the middle as well.)


How is it different from using Clang's CFI (control flow integrity)?

I thought this was the same technique used in webassembly.

Chromium is using this too i think


CFI helps with control flow exploits, but it doesn't prevent memory corruption for example.

This sandboxing technique ensures that both control flow and memory accesses remain in the sandbox (except for when you explicitly allow otherwise).


> First, [hoisting a subcomponent into its own process] requires decoupling the code and making it asynchronous, which is usually time-consuming and may impose a performance cost.

It may need to be asynchronous because of Firefox's peculiar infrastructure for process isolation, but if the intraprocess calls can be synchronous, the interprocess function calls could in principle also be synchronous. By default that's literally how most syscalls work, at least in Unix-like systems. And it's the direction microkernels seemed to have turned--synchronous calls work just fine, and you don't need a bunch of specialized asynchronous calling infrastructure inside the kernel itself.

This seems like a classic case of using a novel solution to subtly redefine the problem. If you just redefined the problem at the outset you wouldn't need so much contrivance.


I am probably missing something: Why is WASM required here? Can't these analysis done directly on LLVM IR?


You're right that this could be done on LLVM IR. The MinSFI project did exactly that basically, several years ago, but it did not see adoption sadly.

The benefits of wasm over LLVM IR is that wasm has already done the work to define the sandboxed format and build the tooling to compile to it. Wasm is also almost as fast as running normally. (Wasm is also portable and lacks undefined behavior, although for this use case those might matter less.)

See the MinSFI section here which compares it directly to wasm for sandboxing:

https://kripken.github.io/blog/wasm/2020/07/27/wasmboxc.html

And the original MinSFI presentation is here:

https://docs.google.com/presentation/d/1RD3bxsBfTZOIfrlq7HzG...


It could be, seems like they already had something in WASM for doing this so it made more sense to use that than re do on LLVM IR.


That's what I though too. C compilers should be able to achieve that directly, and it's incredible that nobody though of doing so yet.

What's great, though, is that they are achieving this with tools that are already available.


LLVM IR is still CPU dependent, and is a moving target. WASM is also a moving target, but much more controlled.


How does this affect process isolation? If only some components can be sandboxed at this fine grained level, aren't we still subject to process isolation to sandbox everything else? It would seem like one still has to run fission.autostart true to isolate the components that cannot be compiled in this way, therefore not gaining the benefit of less overhead as stated in the article.


The purpose of RLBox is to add an extra layer of component-level isolation on top of Firefox's process-based site-level isolation. The reduced overhead is relative to the hypothetical scenario in which we performed the component-level isolation with processes (rather than WebAssembly).


Ohh I see. Not a replacement for process based site level isolation. I just wasn't wrapping my head around that. Makes much more sense now. Thanks for the explanation.


However, without removing processes it will still be as slow as today, which I really hope browsers will do.


I think a more likely way to think about it is that this allows us to sandbox things that would otherwise would not be sandboxable. For a variety of reasons, it's probably not practical to remove the existing process sandboxes.


Reminds me a bit of Apple's AOT protections together with Unity's IL2CPP approach.


I am confused. Isn't WASM supposed to be eventually AOTed (Ahead of Time Compile) or a least JITed? Why this bizarre twist with WASM-C-NATIVE? Browser should do just that instead of these dances around.


This isn't for running WASM from the web. This has nothing whatsoever to do with the WASM JIT that's in Spidermonkey. This is sandboxing for internal components of the browser (or any application really). But, this is a kind of AOT compilation involving WASM in the middle.


I thought that in the wake of Spectre etc, the CPU vendors declared that they wouldn't support privacy guarantees within a process, and that only process sandboxing would be supported?


> Cross-platform sandboxing for Graphite, Hunspell, and Ogg is shipping in Firefox 95, while Expat and Woff2 will ship in Firefox 96.

I wonder what the other "good candidates" that he referred to are.


I wonder how it deals with intrinsics for optimization (SSE and the like), does it fail to compile, or maybe WASM has some support, or it's completely lost in translation?


For something like SSE to work you'd need both wasm and wasm2c to support it.

Wasm doesn't support all of SSE, but wasm does have SIMD support which is a portable subset of common SIMD instructions. You may lose some performance there, but wasm is adding more instructions to help (see "relaxed-simd"). There are also headers to help translate between SSE and wasm SIMD for existing code where possible.

wasm2c does have support for wasm SIMD, although I believe it is not 100% complete yet.


interesting. Are there any resources to get in depth into browser security, specially for a relative novice? also what is the best way to contribute in the sub projects of Mozilla Firefox for example?


> it can’t access memory outside of a specified region

How are segmentation faults handled?


Wasm is defined to trap when it accesses memory outside the sandbox (the embedder can decide how to handle that trap, say by shutting down that particular sandbox).

With wasm2c the trapping can be implemented in a variety of ways, for example using the signal handler trick like wasm VMs do (~14% overhead) or manual bounds checks (~42% overhead, but fully portable).


I believe the implementation in Firefox masks off the high bits of pointers and adds the result to the base address before performing a load/store. This requires us to reserve a power-of-two-sized region of address space, but we can lazily/incrementally commit the pages as the sandboxed code invokes sbrk.


Thanks for the details bholley!

Do you plan to use the signal handler trick eventually? Less portable but in my tests it shrinks the total overhead by half (from masking's 29% to 14%).


Sorry, I should have been more clear. I believe we use the masking on 32-bit platforms, which is faster than explicit bounds checks. On 64-bit platforms we use guard pages. We don't actually need a signal handler, because we don't need to gracefully recover from a fault like we do on the Web — we can just crash.


Nice, yeah, just crashing is even simpler, and sounds good enough...


Can't wait for Firefox XP.


I can’t find it right now but I read a paper that showed that the WASM security model is weaker than native compiled code in some cases. For example, due to compiler and OS hardening techniques, exploits of a libpng flaw weren’t exploitable unless run in WASM. You couldn’t escape the WASM sandbox but the a application itself could be compromised.

I’m sure that this approach is valid as a hardening measure but some of the enthusiasm in the post is perhaps worthy of temperance. This thunk through WASM can’t protect against runtime heap overflows and such.

> However, the transformation places two key restrictions on the target code: it can’t jump to unexpected parts of the rest of the program, and it can’t access memory outside of a specified region

Oof. The paper I recall specifically called these out as not enforceable. The libpng example in the paper directly had an external request have libpng corrupt and access other WASM memory than it owned (in this model it would be other in-process native memory I think or at least the other code placed within the same heap region unless each component gets its own which then means you need to have a fixed memory allocated upfront…).


Are you perhaps thinking of "Everything Old is New Again: Binary Security of WebAssembly"? ( https://www.usenix.org/system/files/sec20-lehmann.pdf )

In any case, I think you've misunderstood the security properties. WASM can have weaker security within the sandbox because it doesn't have access to some of the more sophisticated mitigation measures that native code does, but the security of the sandbox boundary itself is very solid.

The part of the article that you quote is accurate in the sense that I believe it was meant - the code cannot jump to unexpected parts of the rest of the program (outside the sandbox) and cannot access memory outside of a specified region (the sandboxed memory). A vulnerability might allow the target code to jump to somewhat unexpected parts inside the sandbox, or buffer overflows inside the sandbox, but not outside.

As such, it's actually a really effective application of a WASM sandbox!


> WASM can have weaker security within the sandbox because it doesn't have access to some of the more sophisticated mitigation measures that native code does,

And that's mostly read-only data pages. The primary blocker there is how to integrate that capability with ArrayBuffer in the web platform, since Wasm memories can be exposed (or aliased) as ArrayBuffer objects, and most engines aren't prepared to encourage non-writable holes in ArrayBuffers.


Yes! Thanks so much for finding it, that's exactly the paper.

> A vulnerability might allow the target code to jump to somewhat unexpected parts inside the sandbox, or buffer overflows inside the sandbox, but not outside. As such, it's actually a really effective application of a WASM sandbox!

Again, don't get me wrong. This is a very interesting idea to explore. The paper does point out ways that the host JS environment (which in this case can be considered the FF native process) can be exploited. Not as easily as a heap overflow, but not necessarily trivially either. I think both that paper & this post lay out an interesting idea on how to better lock down native extensions.


Triggering a fire inside the castle might be enough to change the output of calls being done into the sandbox, it doesn't need to escape it to be exploitable.


They're addressing this as well - from the article:

> This, in turn, makes it easy to apply without major refactoring: the programmer only needs to sanitize any values that come from the sandbox (since they could be maliciously-crafted), a task which RLBox makes easy with a tainting layer.


Programmer only needs to sanitize....

You mean like all those great programmers that keep introducing bugs like the one recently found out by Project Zero?


> WASM security model is weaker than native compiled code in some cases. For example, due to compiler and OS hardening techniques, exploits of a libpng flaw weren’t exploitable unless run in WASM.

If I understand the post correctly, it's still native compiled code in the end, and it won't run through WASM. The goal of the approach sounds more like a code sanitizer tool to ensure the external library they're using isn't making calls outside of it, or requesting memory beyond the region its given.


What a stupid idea to run bytecode in a browser.

We have gaping security holes with JavaScript already.

Stop the madness.


It's compiling the webasm back to C, so it's not running bytecode.


While your comment is off-topic to this discussion. I find it funny that you think running bytecode in browser is worse idea than the invention of javascript.





Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: