WebAssembly doesn’t make unsafe languages safe
00f.net
00f.net
* ROP should be impossible in Web Assembly, because the call stack is a separate stack, inaccessible from the heap and effectively invisible to the program.
* For the same reason, classic buffer overflow attacks involving attacker-supplied machine code are impossible. In addition, Web Assembly bytecode is never mapped into the heap, so attackers cannot inject shellcode via the usual methods.
* Mmap functionality is proposed for the future [1], which would allow for the implementation of guard pages. (Guard pages are fairly weak defenses anyway...)
I'm sure that it's possible to attack a Web Assembly program's control flow by overwriting vtables and function pointers and taking advantage of the resulting type confusion. But it's significantly more difficult than it is in native code.
[1]: https://github.com/WebAssembly/design/blob/master/FutureFeat...
Because when something is possible in security attacks, it will eventually be exploited.
Besides, nothing stops a wasm compiler from emitting such checks itself, if it wants to.
Having the compiler emiting it wouldn't be an option, as hardware enabled solutions seem to be the only ones accepted by C devs, as shown by SPARC and now ARM.
I do not believe that is a good way of thinking about webassembly. It shouldn't be thought of as an operating system target (like Windows, Linux, or Mac); it should be thought of as an architecture target (like Arm vs x86). What the author wants is an operating system and runtime, which you can write for (and standardize on) web assembly and then load your program into it.
It is a bit surprising, though, that many people do not realize WebAssembly doesn't make C code safe against itself.
If you isolate untrusted code in a WebAssembly VM, that should reduce the attack surface for the system as a whole to whatever functions you expose into the WebAssembly VM.
Personally, my bias is to suggest that using Rust would significantly cut down on (but not eliminate) the attack surface inside the WebAssembly program... but some people don't like to hear that.
WebAssembly does protect the host from a compromised process. WebAssembly doesn't do jack to prevent a process (and whatever data it controls) from being compromised in the first place.
Then I pick SPARC with ADI tagged pointers, as used by Solaris.
Or ARM64 tagged pointers being adopted by Android.
WebAssembly could also provide such feature.
That has always been the very nature of the web.
Web Assembly, like other web applications, will be executed in a sandbox separate from other things. Isolation alone does not make it safe, but it does prevent corruption of other things.
This is why many websites execute advertisements in an iframe, because the code that comes across is not trusted and often not safe. The unsafe code will still attempt to stalk and/or attack the end user, but the iframe prevents that malicious code from corrupting the serving page. There is no reason expect anything different from WASM.
But it will not (and hasn't been designed to) magically remove crashing bugs from your code, just as (e.g.) Javascript doesn't protect you from calling an undefined function.
But those bugs can't do any harm to your machine except crashing the WebAssembly application itself.
*this doesn't mean the sandbox itself doesn't contain bugs of course, but it's the job of the sandbox to prevent stuff running inside it from accessing stuff outside
This is literally the opposite of how the web works. The web is based on the Extensible Web Manifesto[0], where first we come to a consensus and see widespread adoption, and then we make a standard that reflects that consensus.
> We have a fantastic and highly secure execution environment from a host perspective. But from the guest perspective, that very same environment looks like MS-DOS, where memory is a giant playground with no rules.
Well, yeah. Add me to the list of people who are just kind of confused about why fixing C's problems is the responsibility of the browser, and why people thought that it was ever a priority for WASM. WASM is a VM-like isolation chamber for low-level code. It's not designed to make your code safe, it's designed to protect the host environment from unsafe code.
I've seen multiple people make the argument that manually laying out memory in WASM is a design flaw rather than one of the biggest points of the entire implementation. Manually laying out memory with close to zero restrictions or paradigms or safeguards is the thing that makes WASM good.
I feel like I missed something, I don't know where people got this idea that WASM was trying to be a higher level tool. It's an extremely low-level language-agnostic compile target for both safe and unsafe languages, and languages that care about safety should add their own safeguards as they see fit.
The second most common source of process corruption, integer overflow, usually ignored in native targets because direct memory corruption is so much worse, is also adopted into WebAssembly wholesale.
This is all overwhelmingly worse than in the native case, because we make some effort to run only trustworthy native code, but browsers actively solicit unknown code from known-hostile sources--most particularly, web ads.
The existence of safe-ish languages doesn't help, because there is no incentive to deploy them in user-hostile code fragments.
WebAssembly strongly sandboxes WASM modules that are executed, so the situation where an attacker creates and serves a malicious WASM module to the user's browser is one that is handled well.
The article is saying that WASM's protections stop a module from escaping its sandbox, but it doesn't prevent a buggy module from having its own memory get corrupted by its own memory-related bugs common in unsafe languages.
Furthermore, induction and compiler optimizations can remove the need for such checks. Aren't you solving this at the wrong layer of abstraction?
The C standard allows for the NULL pointer to be numerically any value, not just 0, and on some architectures in isn't 0. So I would hope C compilers targeting wasm would just use a different value for the NULL pointer, eg 0x8000000000000000 not 0.
Whether they actually do or not I don't know, but it isn't an architectural failing of wasm.
The author's argument that this is a mistake is true. WASM should almost certainly have had some arbitrary offset like 0x1000 just due to the extreme commonality of null pointer derefs, and changing NULL to something else won't come close to solving that problem. You still have pointer derefs of fields in structs that were calloc'd or memset(0)'d. Compiler can't magically swap out the writes-to-zero there with whatever NULL should be instead.
They then also have ways to mark pages as read-only and such, but they don't burn a bunch of pages on this for NULL pointer catching. They simply just don't map anything before the program offset.
This is serious enough an issue that for debugging purposes emscripten has a SAFE_HEAP mode which will check for NULL pointers on every access. We'd still need that to look for 0 even if the compiler considered NULL to be something other than 0.
Which, to be clear, is not a valid assumption wrt portable C or C++.
if (!p)
Anywhere would immediately cause problems. An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.
I don't think this definition excludes platforms that use non-zero bit patterns. A constant 0 in C code should get converted to the appropriate (possibly non-zero) bit-pattern for the platform.Also, my understanding is that whether the platform has useful things at address 0 falls in the category of "implementation defined".
It doesn't; the comment above yours is saying the same thing as you are.
If address zero is a null pointer, then IIRC no allocated object, whether static, automatic or heap, can have that address - i.e pointers to them must always compare unequal to nullptr.
If null pointer is something other than address zero, then it's just a regular address, and can have anything - it's not even "implementation defined", not anymore so than saying that e.g. 0x12345678 is a valid object address on x86.
It would be quite hard to spec malloc/free into the Web platform, because of the necessary detail: the behavior of that malloc/free pair would need to be identical (same pointers returned from malloc) in all browsers, for every possible sequence of allocations and deallocations. GC is actually easier to spec since so much of the underlying details are unobservable.
It's not impossible in theory, but it seems like it would mandate all browsers use the exact same allocator implementation (say, dlmalloc version x.y.z). In addition, this would not allow optimization over time, again, unlike GC.
In the non-browser case, server-side implementations may not need to worry about specs, so more options may be open there.
Why would you ever spec it like that? That doesn't seem needed at all. Plenty of programs run across platforms with wildly different mallocs and everything runs file. Replacing the malloc with LD_PRELOAD tricks is even done sometimes.
You get similar benefits by using a decent OS and/or by putting the untrustworthy code in e.g. a docker container. It's still code that can't be trusted but at least you are setting some hard boundaries that have no easy/known ways of being bypassed.
However, it would still be bad if the program gets compromised. If, say, you are handling some credit card details and are using some C code to do that and it gets compromised, all of the interesting stuff (i.e. the credit card details) would be inside those boundaries. This is why using C to do that is not a great idea.
This is a concept that is often misunderstood by people: most of the interesting stuff (from a hacker point of view) happens inside the sandbox. This is where you access and handle sensitive data and access protected resources (e.g. a third party website). Many attacks use relatively low tech mechanisms such as script injection, man in the middle attacks, social engineering, etc.
Wasm indeed does nothing to protect against that. If you are handling user input in any way, that is potentially a way for hackers to inject code in your sandbox. If that sandbox has access to or control over anything interesting, that just got compromised. If you are using a language that is notorious for its decades long history of input validation mechanisms getting compromised (cough C cough), that means you can't trust input validation to function properly even inside a wasm container.
This is exactly how many browser attacks work. They don't install viruses on your machine or whatever but they simply trick you into revealing your credentials, visiting some evil website, or entering your credit card. A bit of injected javascript or a redirect is all that this takes. Any injected code never leaves the sandbox; it doesn't have to.
There used to be the possibility to restrict the C++ language to a safe subset via /clr:pure and clr:safe switches, but now Microsoft official position is that C# or other .NET languages, are the only way to effectively write safe and verifiable code.
https://docs.microsoft.com/en-us/cpp/build/reference/clr-res...
https://docs.microsoft.com/en-us/cpp/dotnet/pure-and-verifia...
Is that really true? I think we can implement a memory allocator done by hosts. By using "import" feature of WASM, a guest could implement a memory allocator that imports functions from the host that ask the host to look through the gest's memory directly (it is possible in JS code) and then allocate one. Maybe it will be slower but it is certainly possible that hosts can know what is happening to the memory in this way.
We can already run most popular languages pretty much anywhere, and I imagine at better performance than WebAssembly, no?
Embedded scripting languages (JS / Lua) are platform-independant and sandboxed, but much slower than native code.
C is fast and small. It can be very carefully sandboxed by the OS (eg iOS apps) but the sandbox has historically been very leaky. Also binaries must target a specific hardware architecture.
The JVM & .NET environments are platform independent, but their heavy reliance on garbage collection slow them down. They also both have large runtime environments which makes them more awkward to embed. I wouldn't want to embed the JRE inside the linux kernel or in a web browser because its so big and it has such a terrible security track record. But WASM would work great.
What other technology would be a good fit for making mods for a multiplayer game? Java? Too bloated. Lua? Workable but slow. C? You can't ship 3rd party mods safely and even if you could they would need to be recompiled for every platform. But WASM will work great. What other tech could you use to run a user-supplied filesystem driver in-kernel? Everything else is too slow (lua) or too unsafe (C).
I'm starting to see the value in using WebAssembly outside the browser.
iirc you can have multiple instances or modules, each with its own private linear memory, and they can be set up to call each other's exported functions. It's just that current compilers don't use that encapsulation when compiling a single program. Am I wrong about this?
(I haven't used WebAssembly, only read about it some time ago. But here's a link in support: https://groups.google.com/forum/#!topic/e-lang/3A6zYWF6u5E "Multiple module instances only share an address space if they explicitly export/import a linear memory, which is just another kind of module-level definition. A module can define its own linear memory and not export it, in which case nobody else can access it.")
A normal C program has full reign over the entire process memory space.
A wasm program only has access to the call interface provided to it and its linear memory region allocated to it.
The guarantees are much different and greatly in favor of wasm. In wasm the stack and the code segments are off limits.
And the C heap is typically a linear memory region (that grows monotonically) isn't it?
The "not faulting on address zero" thing is interesting but not crazy -- only broken programs will break.
brk is an ancient holdover from when memory protection was done via segmentation. Paging eliminates the need for this historical misfeature. I'm a bit sad WASM propagated it further into modern systems.
I guess your allocator can try its best to prevent fragmentation, but you're right that returning memory to the OS isn't going to happen without explicit compaction (which nobody in native code is going to do.) Dunno if anyone ever expects memory use to go down though.
(Is less true than it used to be? -- I guess we used to make big swap files and now that's less fashionable.)
I guess if it's not too much more of a burden on the OS to make it keep track of a Swiss cheese virtual memory allocation then it is useful.
The brk system call places an unnecessary restriction on how the memory is used that is not imposed by the hardware. Back in the 70s when segmentation was a thing and virtual memory hadn't taken hold this would be different - but virtual memory has won.
FreeBSD straight up did not implement sbrk at all for aarch64 and riscv – very very few things broke because of that (one of the things that did break was, IIRC, GNU Emacs).
C only has access to its process space as does WASM. WASM's only improvement is it makes stack smashing bugs harder (which ironically makes real security worse as it means you can't use retpoline, but let's ignore that for now)
HOWEVER it gives up on using guard pages and address randomization (systemic preventions against common bugs like buffer overflows), making WASM in practice less safe than C.
WASM is only safer for the host embedding untrusted code. For trusted code, though, it's across the board worse in every way - including security. Which shouldn't be all that contentious of a statement since WASM had no goals or intentions to ever replace trusted native code execution. That's not a thing it tries to do, and it shouldn't be a surprise that it didn't do it.
Why wouldn't the WASM JIT retpoline all code (probably only if the underlying processor needs it) rather than relying on the guest programs to do it themselves.
> HOWEVER it gives up on using guard pages and address randomization (systemic preventions against common bugs like buffer overflows), making WASM in practice less safe than C.
Once again, wouldn't the JIT be able to enforce this at runtime?
It's not like WASM binaries are referencing raw address offsets.
Possible, but is that then a runtime option in the WASM header? How does it know if the program needs the cost of retpoline or not? And how can a program ensure that it's getting such mitigations when it needs them?
> Once again, wouldn't the JIT be able to enforce this at runtime?
The JIT has no insight into the app's malloc/free usage, so no, it can't. There is no system allocator in WASM, just a big chunk of linear memory that the app does whatever it wants with.
For an actual case study in why this is risky all you have to do is look at OpenSSL's heartbleed. If it had been using the system's malloc/free (which WASM doesn't have) instead of using its internal free lists then heartbleed largely wouldn't have existed on platforms like OpenBSD.
You're looking at it the wrong way. Untrusted code doesn't have a "I pinky promise that I'm safe" bit they can twiddle in their header. If the code running is untrusted, it doesn't get to make that decision, all code under that context needs to be retpolined. (V8 already does this https://chromium.googlesource.com/v8/v8/+/7d356ac4927e9da3e0... )
> The JIT has no insight into the app's malloc/free usage, so no, it can't. There is no system allocator in WASM, just a big chunk of linear memory that the app does whatever it wants with.
It can retarget the code so that the virtual base address of the linear region isn't fixed. That's about all ASLR does anyway.
> For an actual case study in why this is risky all you have to do is look at OpenSSL's heartbleed. If it had been using the system's malloc/free (which WASM doesn't have) instead of using its internal free lists then heartbleed largely wouldn't have existed on platforms like OpenBSD.
You can implement all of the malloc protection strategies that OpenBSD uses in user space. It's completely legitimate to use your own malloc in those circumstances, libc's malloc isn't magically special, and it makes sense that they'd want to rely on memory allocation with additional guarantees than libc gives you. OpenSSL's sin wasn't re-implementing malloc; it was re-implementing it poorly (and not bounds checking input from attacker controlled packets).
Otto Moerbeek has a great recent Twitter thread on the topic: https://twitter.com/ottom6k/status/1065594523559096321
https://twitter.com/ottom6k/status/1063118597160091648
> The JIT has no insight into the app's malloc/free usage, so no, it can't. There is no system allocator in WASM, just a big chunk of linear memory that the app does whatever it wants with.
This point by kllrnohj illustrates a pretty significant weakness.
Additionally WASM is moving towards multiple linear regions, which should allow mmap/munmap like functionality.
And in the context of OpenSSL, this kind of fanciness in some libc's that'll make all sorts of syscalls randomly is exactly why they wrote their own allocator.
> And in the context of OpenSSL, this kind of fanciness in some libc's that'll make all sorts of syscalls randomly is exactly why they wrote their own allocator.
"web asm -- if it was good enough for openssl!"
> "web asm -- if it was good enough for openssl!"
More like, "OpenBSD, so slow that people feel the need to write their own malloc".
No, you're not understanding.
retpoline doesn't keep you from attacking others, it keeps you from being attacked. But not everything handles sensitive data and thus doesn't care about being attacked, and doesn't want to pay the cost of the retpoline that they didn't need at all.
How can a WASM app signal "I need retpoline because I have secrets to protect" from "I don't give a damn about retpoline because there's no secret data in my process space at all"?
That's not a decision WASM can unanimously make on behalf of others.
> You can implement all of the malloc protection strategies that OpenBSD uses in user space.
No, you literally can't. WASM doesn't have the APIs necessary to do that. It doesn't have mmap & mprotect. Those are in the "proposals we might consider" future section of WASM. But WASM today cannot implement a malloc/free on-par with those in actual libc implementations on major platforms. To say nothing of debug tools like valgrind.
> OpenSSL's sin wasn't re-implementing malloc; it was re-implementing it poorly (and not bounds checking input from attacker controlled packets).
So the solution to people re-implementing malloc badly is to force everyone to re-implement malloc? How does that follow at all?
I get WASM doesn't want to include malloc/free, but the problem is it didn't include paging-based allocation APIs. It used a nonsense linear growth heap design that doesn't match anything about how memory works on any platform and has been obsolete for 20+ years.
This isn't a big deal in the hyper specific context that WASM is actually targeting, but it is a big deal if you're talking about shoving WASM in places it never had any intention of being.
I literally linked to the commit where V8 is indeed unanimously making that decision. If you clear the indirect branch table when transitioning to and from all untrusted programs, what's your threat model again?
> I get WASM doesn't want to include malloc/free, but the problem is it didn't include paging-based allocation APIs. It used a nonsense linear growth heap design that doesn't match anything about how memory works on any platform and has been obsolete for 20+ years.
Go read the WASM spec. It's explicitly is designed for multiple linear regions, just how mmap works. It just didn't make it into the MVP.
And for things where retpoline isn't needed it's unambiguously wrong (wrong in that runtime is slower than it should be for no gain). What are you not getting about that? They made a decision that when it's wrong it's still safe, but it's still going to be wrong for a non-trivial amount of users.
> Go read the WASM spec. [...] It just didn't make it into the MVP.
Yes, I know, that's why I said what I said. Which was correct and supported by both the spec and your statement just now...?
We're not talking about hypothetical future WASM, we're talking about WASM as it exists today and what problems it can/can't effectively handle.
[edit this was not snark, I believe the parent has important insights]
I believe it is possible, but the severity seems orders of magnitude less than native code.
The host controls the VM interface boundary. Native code can only get this partially via process isolation.
It also has a reasonable use case ( or did, pre Spectre).
It allows you to hoist and speculate loads that may be null. Since it is valid memory, instead of a fault, you would just get a result (usually zero on most of these platforms) you would throw away if it was not needed.
I feel this the most important point regarding use server-side.