> Similarly, disabling the JIT compilers would also only be a partial solution: historically, roughly half of the bugs discovered and exploited in V8 affected one of its compilers while the rest were in other components such as runtime functions, the interpreter, the garbage collector, or the parser. Using a memory-safe language for these components and removing JIT compilers could work, but would significantly reduce the engine's performance (ranging, depending on the type of workload, from 1.5–10× or more for computationally intensive tasks).
If you're willing to take the performance hit, Chromium actually allows you to disable JIT easily in the Settings and add exceptions for certain sites. Open the Settings and search for V8 Optimiser.
> while the rest were in other components such as runtime functions, the interpreter, the garbage collector, or the parser
Notably memory safe languages wouldn't really help with the garbage collector since it would have to use unsafe Rust & the confusion about lifetime would still exist or you'd be using something like Java/C# where you're just relying on the robustness of that language's runtime GC.
However, the runtime functions, interpreter and parser would be secured by something like Rust & I fail to see how well-written Rust would introduce a 1.5-10x overhead.
That’s not quite true. It only depends on what level of abstraction are you willing to do — you can write a runtime with GC entirely in safe rust (or a managed language).
It doesn't matter if the JIT itself is written in a memory-safe language or not if you're exploiting miscompiled JIT output. If the machine code emitted by a JIT is wrong, it can be exploited regardless of if the JIT itself is memory safe or not.
Surprisingly, it does matter! The key abstraction here is partial evaluation combined with memory safe languages.
Look at how the JVM world does it with GraalJS (or GraalPython or the other langs). This approach eliminates the sorts of vulnerabilities V8 is talking about, because the semantics of the language are defined as a memory-safe interpreter, which is then converted to JIT compiled code directly by having the compiler treat interpreter data structures as constants for the constant folding passes.
This gives you a 1-2 win:
1. The example given at the top of the blog post wouldn't be exploitable in interpreter mode in GraalJS, because the VM intrinsic would be written in Java and thus bounds checked.
2. It's also not exploitable once JIT compiled, because the intrinsic and its bounds check has been inlined into the user code function and optimized as a unit (meaning the bounds check might be removed if the compiler can prove that the user's implementation of ToNumber doesn't modify the size).
GraalJS has nearly V8 levels of peak performance, so this technique doesn't require big sacrifices for server workloads, and client workloads where latency matters more are now a focus of the team. In this way you can build a high performance JS engine that still has tight security. It supports code sandboxing also.
(Disclosure: I do part time work for Oracle Labs and sit next to some of the people doing that work)
Yup you can write a garbage collected interpreter for a programming language with no unsafe code at all, even for languages that have complex data structures like doubly linked lists in them.
Using something like a slotmap to store the languages objects in is what I would do, and your GC would just involve removing values from the map after marking everything that's reachable.
The popular slotmap crate on crates.io does contain unsafe code but nothing about the data structure inherently requires unsafe.
Yes, it also disables WASM, MP3 decoding, gamepad API, JPEG 2000, SVG fonts, PDF previews, WebGL, Speech Recognition API, Web Audio API. Pretty much web as it was meant to be ;-)
> would significantly reduce the engine's performance (ranging, depending on the type of workload, from 1.5–10× or more for computationally intensive tasks).
And the downside being?
Seriously, JS was never meant to be performant. In the real world, it's very rarely used for anything computationally intensive.
If you mean “wasn’t originally meant”, that might be true. But it’s been meant to be performant for quite a long time, with huge investments behind the realization of that intent.
It’s fine if you have nostalgia for whatever you think was the original vision behind JS. But that hasn’t been the operating vision for it for many years.
Very curious what your unique definition of 'computationally intensive' is, that manages to not include one of the most significant computational workloads worldwide, both in terms of absolute volume and impact on human productivity. Namely, web browser rendering performance.
Huh? The rendering is part of the browser itself. It's not like JS has to run every frame to put the pixels onto the screen.
Making ajax requests and doing things to the DOM tree isn't a "computational workload" because there's hardly any computation happening in that JS code.
Rendering pages server-side and avoiding 5-megabyte bundles might help with that. JIT and other browser-side performance optimizations just delay the problem anyway. The culture around web development needs to change.
Fly uses Firecracker micro VMs rather than V8 isolates. Two of the engineers behind both services had a friendly discussion about it a few years ago: https://news.ycombinator.com/item?id=31759170
It’s much more isolation than C threads – the entry point for a thread is a whole module (not a function), and threads must use message passing to communicate. They can share memory, but only via [Shared]ArrayBuffer objects. They're in the same OS process, but each have their own global process object.
But I think it'd meet your need for an in-process isolated execution environment, which you can terminate from the main thread after a timeout.
If you use SharedArrayBuffers in worker threads, use Atomics.wait to block, and then terminate the worker which would've called Atomics.notify – yes, if no timeout is used on the notify.
But I don't know of any other way this would happen.
V8 isolate termination is more akin to throwing an uncatchable exception, versus killing a thread abruptly. When you ask v8 to terminate an isolate, it does take some time for it to terminate depending on what code is running.
> The V8 Sandbox has already been enabled by default on 64-bit (specifically x64 and arm64) versions of Chrome on Android, ChromeOS, Linux, macOS, and Windows for roughly the last two years.
It seems that the problem is in Javascript itself, where almost every action can cause side-effects. This makes interpreter extremely complex and it is difficult to spot a mistake. Converting something to int shouldn't cause any side effects.
// At index 100, the @@toPrimitive callback of |evil| is invoked in
// line 3 above, shrinking the array to length 1 and reallocating its
// backing buffer. The subsequent write (line 5) goes out-of-bounds.
array.fizzbuzz();
```
I'm probably missing the point, but I thought indexing into an array in Javascript outside its bounds would result in an exception or an error or something?
Those are the intended semantics of JS, but that doesn't help you when you're the one implementing JS. Somebody has to actually enforce those restrictions. Note that the code snippet is introduced with "JSArray::buffer_ can be thought of as a JSValue*, that is, a pointer to an array of JavaScript values", so there's no bounds checking on `buffer_[index]`.
It's easy enough to rewrite this C++ code to do the right bounds checking. Writing the code in Rust would give even stronger guarantees. The key point, though, is that those guarantees don't extend to any code that you generate at runtime via just-in-time compilation. Rust is smart, but not nearly smart enough to verify that the arbitrary instructions you've emitted will perform the necessary bounds checks. If your optimizing compiler decides it can omit a bounds check because the index is already guaranteed to be in-bounds, and it's wrong, then there's no backstop to swoop in and return undefined instead of reading arbitrary memory.
In short, JIT compilation means that it's ~impossible to make any safety guarantees about JS engines using compile-time static analysis, because a lot of the code they run doesn't exist until runtime.
OOB indexing from the Javascript side would return undefined, but OOB indexing on the engine side (lines 5/7/9 of JSArray::fizzbuzz()) is the same as OOB indexing a pointer
The example fizzbuzz() function is implemented in C++. (And out-of-bounds indexing in JS actually doesn't generate an exception/error; it just returns undefined. Great language!)
Disclaimer: I have very little experience with C++, a bit more with Rust code that bridges with JS in a manner similar to the example, and zero experience with V8 dev. All of that said…
I think the technically correct responses you’ve gotten so far may be missing an insight here: wouldn’t the V8 example code be just as safe as the equivalent JS if it used the JS array’s own semantics? More to the point: presumably those JS semantics are themselves implemented in C++ somewhere else, and this example is reimplementing an incorrect subset of them. While it’s likely inefficient to add another native/JS round trip through JSValue to get at the expected JS array functionality, it seems reasonably safe to assume the correct behavior could be achieved with predictable performance by calling into whatever other part of V8 would implement those same JS array semantics.
In other words, it doesn’t seem like you’re missing the point. It seems like this kind of vulnerability could be at least isolated by applying exactly the thinking you’ve expressed.
> wouldn’t the V8 example code be just as safe as the equivalent JS if it used the JS array’s own semantics?
Yes, but imagine that the code we are talking about here is JIT code that the compiler emitted. If the compiler JITed code that was safe Rust then it could be safe. But JITs emit machine code and a big part of their performance is exactly in the "dangerous" areas like removing unneeded bounds checks.
Say you have a bounds check in a loop. In some cases a JIT can remove it, if it can prove it's the same check in each iteration. Never removing the check would be safer, of course, but also slower.
The point of the article here is that a lower-level sandboxing technique can help such JIT code: even if a pass has a logic bug (a bug Rust would not help with) and removes a necessary check then the sandboxing limits what can be exploited.
then the array's length is not read once at the beginning of the loop; it is read every loop iteration. So if the code inside the loop (or inside the getter for `length` itself) modifies the length of the array, then it will be caught when the condition is evaluated. The problem is that the C++ code makes assumptions about JS (reading the length of an array cannot change the array's length) that don't hold. But it's an easy mistake to make.
> neither switching to a memory safe language, such as Rust, nor using current or future hardware memory safety features, such as memory tagging, can help with the security challenges faced by V8 today.
And here I thought Rust would fix my security issues ...
JIT engines are fundamentally unsafe since they would produce unsafe machine code directly. And for performance sake the runtime should use tactical unsafe. So memory safety is definitely still something to worry about, even if it's less likely to occur in my experience
They say that Rust is not enough and dismiss it quickly:
> V8 vulnerabilities are rarely "classic" memory corruption bugs (use-after-frees, out-of-bounds accesses, etc.) but instead subtle logic issues which can in turn be exploited to corrupt memory. As such, existing memory safety solutions are, for the most part, not applicable to V8. In particular, neither switching to a memory safe language, such as Rust, nor using current or future hardware memory safety features, such as memory tagging, can help with the security challenges faced by V8 today.
> There are a lot of use-after-frees and out-of-bounds accesses, buffer overflow in there.
Yes, and they’re in the runtime itself, which rust cannot protect you from. Rust cannot protect lifetime enforcement for GC objects any more than C++ already does, it can’t protect you against OoB when the reason for the OoB is the runtime is wrong about the object size, etc.
Rust does not magically make it impossible to have errors, it makes it harder by default, but the cases where these go wrong are already largely using c++ to provide the same level of memory safety rust can in the environment.
The easiest way to understand this is if you use `vec` you won’t get unsafe oob, but if there’s a bug in `vec` rust (or any language) cannot protect you. Eg if there’s a JVM bug that breaks arrays then the fact that Java is memory safe isn’t relevant.
Also worth pointing out that the specific problem area, highly optimized runtimes for interpreted/JIT-compiled languages, the borrow checker doesn't really have much to offer. Rust's safe memory paradigm more or less requires an "owner" for every pointer, and by definition the arbitrary graph of pointers in the executed code aren't going to have that. Any such runtime is going to be built on a metric ton of unsafe.
I would argue that this is trying to fit a square peg in a round hole: The JS objects should not be considered "owners" for each other in the Rust ownership sense. The Isolate / heap uniquely owns all items within it: The arbitrary graph of "references" between objects are merely data for the garbage collection algorithm and keys that can be used to get items from the heap. They have no real way to actually access the item "pointed to" by the reference directly. All access must go through the Isolate (at least in a lifetime sense).
This would allow writing the system with either very little or no unsafe code (depending on the accessing method and heap structure chosen), and Rust's borrow checker would actually correctly tell you that you cannot hold an Array's buffer borrowed while calling some unknown function because the function can mutate anything in the heap and thus needs to take exclusive access to it.
EDIT: Also note that V8's "sandboxed pointers" access all already goes through the Isolate indirectly: All compressed pointers as part of their decompression use a "base" pointer. The same could be written (with unsafe) in Rust in such a way that the "base" pointer is the heap pointer that carries all ownership of the heap, and now decompressing pointers would be equal to taking a borrow of the whole heap.
This is not strictly true. If you took a magic Rust wand to the whole V8 codebase that converted it into exactly the equivalent Rust code, Rust would definitely not help you one bit. But the code would also be filled with unsafe blocks. Many commenters have been saying that this is because it must be, otherwise the engine couldn't be written. For the JIT side of things I definitely agree. But as for the runtime's static implementations (those written in C++/Rust) it is entirely possible to write the runtime in safe Rust which definitely would catch eg. the simple example error they use in the blog post.
One option would be to use an existing gc crate: It checks at runtime that Rust's borrowing rules are upheld, so it would abort the program instead of doing the OoB access. This is of course not nice but it is memory safe. Obviously this would also make the engine less performant as now we're doing extra work on every read and write into a GC object.
Another option is to let go of our idea about GC objects holding mutable pointer access to one another. V8 already uses offsets from a 4 GB offset to find the item; these are "compressed pointers" because they always know they need to just upshift a bit to get the actual pointer, and because C++ doesn't care about multiple objects holding pointers to one another. A Rusty alternative to this would be that the "compressed pointers" are considered only 32-bit offsets from some base pointer that is held by the Isolate: Now Rust would not allow these to be actual pointers or become references. Instead you'd need to implement some API at the Isolate that gives you a reference to an item based on that offset, and the reference's lifetime is determined by your Isolate's lifetime.
Now, any call to JavaScript is always (in a theoretical sense) capable of mutating anything within the Isolate's heap. This means that calling a JS function would require an exclusive `&mut Isolate` reference: This now means that Rust understands that it cannot hold a reference to the Array's buffer (which it got a reference to from the Isolate heap) during a call to a JS function.
This sort of API would internally need some unsafe because it is a pointer offset we're doing here. If you don't like that, you can also go a step further and build your heap as a collection of vectors and then have your "pointers" be a combination of a type key and an index an index into that type's vector. With this sort of heap structure there is no unsafe usage needed as getting a reference is just borrowing from a Vec within the heap.
These "Rusty" ways of building the heap offer some interesting philosophical thoughts to ponder: The V8 way is kind of a faithful structuring of the ECMAScript concept of "object ownership" in C++: Items refer to each other directly, and can even do it recursively. Ownership of the memory is just kind of... there. It's obvious, right? This object refers to that, so it owns it. Except maybe if they're recursive and not accessible elsewhere... I mean, just don't think about it! (Unless you're building the GC algorithm.) The safe Rust heap structures make the memory ownership quite concrete: The Isolate owns all of its GC objects / everything in its heap. GC objects and references between them are NOT actual memory ownership relations! This JS object referring to that JS object means nothing to the memory ownership. A JS object exists as long as the Isolate thinks it exists, even if no other object refers to it.
So, the C++ / unsafe Rust way to write the Heap tries to unify JS ownership logic and the host language ownership logic into one. When these two don't agree, bugs can happen (like in the simple example provided: JS Array semantics written naively in C++ cause issues because C++ has different expectations). The safe Rust way instead throws the JS ownership logic out of the host language entirely and forces the engine to implement that runtime ownership logic on its own.
Source: I am writing a JS engine in (safe) Rust using the "heap is a collection of vectors of Ts" method.
Sure, but a bug in Rust's vec is unlikely at this point & thus as long as you're in safe Rust you have no possibility of a memory error, which isn't the case for C++ vectors.
It can't protect you from lifetime issues with GC objects, but it can for almost everything else you're doing. They indicate 50% vulns are JIT and 50% are memory safety issues in the runtime, where GC is only part of it. If the bulk of the runtime issues are around GC lifetime confusion, I agree that Rust maybe wouldn't help. It might help to make sure you don't misuse the GC machinery which might be a significant mitigation, but given the bugs I've seen in the field around integrating with the GC, I doubt Rust would help with that class of bugs.
>Sure, but a bug in Rust's vec is unlikely at this point & thus as long as you're in safe Rust you have no possibility of a memory error
It has nothing to do with the built-in data structures because it doesn't even exist in the same space as them. The flaws themselves are in an algorithm's reasoning, it's not an issue that exists because somewhere in the codebase there's an out-of-bounds access on a vector. The issues are caused by said flawed reasoning generating bad machine code with erronious pointer arithmetic. Note that it's the reasoning itself generating bad pointer arithmetic, not pointer arithmetic that exists explicitly in the codebase.
It's the kind of problem you need proof systems to solve. A substructural type system (or a near-approximation like Rust's ownership semantics) is simply not robust enough for the problem domain, you need full blown dependent types for this kind of thing, something that can guarantee logical safety.
Have you read the article? It's about the mitigation system (the heap sandbox) they have in place to limit the bugs in the JIT can inevitably generate, not about improving the security of the generated code.
Correct, Rust won't help with the JIT part. But it would help with the sandbox escape which is the 2nd exploit that has to be paired with a JIT exploit now. As they noted, these sandbox escapes are primarily dealing with trivial memory safety issues that Rust would just make impossible to begin with, thereby significantly raising the efficacy of the sandbox mechanism.
The problem with the JIT? Sure. I'm talking about the sandbox escapes which is what you now need to pair JIT exploits with to get an RCE in the renderer. Rust would help eliminate those sandbox escapes more effectively than trying to continue to harden the C++ codebase.
You’re missing the point. You’re right there are unlikely to be bugs in vec, but there are also unlikely to be bugs in std::vector or WTF::Vector both of which error out on OoB (chrome/v8 uses hardened libc++).
I was using `vec` as an example of runtime code that is fundamentally implemented in unsafe code. The errors that are being discussed are errors in the runtime - eg the unsafe{} blocks of rust. It’s very difficult to write code in v8/blink (or JSC/webkit) that interacts with the relevant JS runtime in ways that make the code unsafe - just as you cannot normally interact with `vec` in a way that causes a memory safety error - however the runtime’s implementation of the safe interface is still has to eventually perform unsafe operations. The bugs that you see in V8, JSC, etc are almost invariably in code that would necessarily be unsafe region in rust that would not be preventable in rust.
Another example: `Arc`, `Rc`, and `Box` etc all allocate memory, and all your rust code can be built on those, and be safe (assuming no bugs in the refcounting, no compiler lifetime errors, etc), but the allocator beneath them still has to do everything correctly and the operations it performs are largely unsafe. There’s nothing rust can do to prevent a logic error from returning overlapping pointers. You can create lots of abstractions to make it harder to screw up, but you are the runtime at this point so the code that is requiring safety rules is also the thing specifying those rules. Eg if the erroneous state/logic that leads to an incorrect allocation is the same state/logic you are testing against to ensure you aren’t making an erroneous allocation. You can see how that impacts the safety profile of the code.
When JSC or V8 have a use after free vulnerability it’s almost always a runtime error because the overwhelming majority of allocations made by both engines are via their own GCs, and so definitionally should be sound. But if there’s a bug in the runtime (a missing barrier, or a scanning error in JSC), then objects can be erroneously collected and that’s how a UaF happens. There’s nothing rust or any safe language can do to make those errors impossible or unexploitable. All the runtime can do is structure the code to make errors as hard as possible, in rust that means minimizing the amount of time in unsafe{}, and add mitigations such that any error that does happen is hard to exploit.
When V8 and JSC have buffer overflows it’s because the metadata for an object says “there is this much memory available” but that is incorrect. Again rust cannot protect against this: you’re in the position of a `vec` with incorrect bounds information.
And that goes on for all the types of bug class. The vast majority of the security benefits rust offers for a language and vm runtime are available - and used - in c++. The bugs are in the code that would necessarily be unsafe{}.
Now in blink/webkit the moment you get beyond the relevant JS runtime you run straight into the standard C++ nightmare that rust, swift, JS, C#,… prevent so that’s another thing altogether.
You've said a lot and a lot of it is accurate, but none of it really applies to the sandbox which is what this blog post is about. Here's the most relevant part of the article:
> This code makes the (reasonable) assumption that the number of properties stored directly in a JSObject must be less than the total number of properties of that object. However, assuming these numbers are simply stored as integers somewhere in the JSObject, an attacker could corrupt one of them to break this invariant. Subsequently, the access into the (out-of-sandbox) std::vector would go out of bounds. Adding an explicit bounds check, for example with an SBXCHECK, would fix this.
> *Encouragingly, nearly all "sandbox violations" discovered so far are like this*
Emphasis mine.
This sandbox is about injecting an indirection layer to protect against those JIT issues (which Rust doesn't help with) from being used to escape the isolate's memory. What that means is that JIT issue has to be combined with a sandbox escape to have the same exploit as just the JIT without the sandbox.
Thus, sandbox escapes are a real concern & a critical part of the security model. Those happen because of a memory safety issue in the C++ code, not because of the JIT. That's 100% by design because if the JIT bypassed the sandbox the sandbox wouldn't do anything.
A Rust sandbox written with `#![forbid(unsafe_code)]` wouldn't have these issues. It might still because even safe Rust isn't 100% guaranteed to be sound due to compiler bugs, but now you're having to pair a JIT issue with a compiled Rust memory safety issue which is much much harder. It's going to be at least an order of magnitude more reliable than C++ even with a hardened libc++. That being said, I don't believe the sandbox alone would be enough. I believe `JSObject::GetPropertyNames` is in the runtime & that again isn't directly invoked by JIT nor is it code that requires unsafe.
Type confusion is also a very common attack against JS runtimes and V8 specifically. Of course, it's not trivial to build a high-performance JS runtime without playing around with pointer types pretty liberally, so I can understand saying "Rust won't fix this" in regards to those attacks.
But those attacks would basically not be possible against a runtime built on top of Java or C#.
Yes because the attack would be against the .net or Java VM.
The JVM - especially in the era of applets - had an illustrious history of VM bugs. We don’t know how bad they would have been because in the era of extremely complex exploits applets essentially do no exist. Neither .net nor the jvm are exposed to the degree of attacks the js engines are, and there’s no strong reason to believe they don’t have similar bugs today.
I'm not singing the praises of the JVM here, it's just a simple fact that if you implement your runtime in a higher level language you're exposed to a smaller number of potential vulnerabilities. Unchecked array dereferences turn into bounds-checked array dereferences; unchecked typecasts turn into checked typecasts. Null pointer dereferences turn into null reference exceptions. Etc.
Of course once you start jitting native code, all of that is off the table. Unless you jit to java/.net bytecode, I guess.
No, you're missing the point. The whole point is you're implementing the runtime that defines the safety semantics. Your proposal is essentially "implement your JS engine GC on top of the JVM by just using the JVM's GC", i.e. don't implement the GC yourself. The unsafe code is now the JVM GC, and you've just moved the problem from "implement the JS engine's GC" to "Implement the JVM's GC", and they same problems continue to exist.
I am really struggling to understand where this gap in understanding is occurring. It does not matter what environment or language you implement a JS engine (or whatever) in. The attacker is going to attack the unsafe portion of the runtime. If you build you JS engine on top of the JVM, then the attacker is not going to attack your JS engine's runtime, they attack the JVM's.
The JVM, .NET, etc runtimes are not doing anything different to what the JS engine runtimes are doing, and aren't magically free of the same bugs. If anything they're probably doing less to protect from or prevent attacks, because they have a much much smaller attack surface (because they aren't generally exposed to everything on the internet) and the reason attackers have to target the JS engine runtime is because the JS sandbox does not allow the general system access "correct" and completely uncompromised .NET or JVM code have. Attacks on the JVM and .NET generally mean "convince the VM to load correct code that does something that a specific app/service is not meant to do but the VM generally allows applications to do", whereas a JS VM does not allow an attacker to do anything outside of the JS sandbox, so they must compromise the runtime.
It may be easier to understand if we try to present this in a different way:
JSC can be compiled as an interpreter for any cpu architecture because there is a fall back C backend for the interpreter code generator, so you can compile JSC to WASM. Then you could make a version of webkit than executed all JS through the WASM build of JSC running under the native JSC runtime. You've now built your JS engine on top of a safe runtime (WASM), but it should hopefully be obvious that an attacker is simply going to continue targeting the native JSC runtime.
People have previously shipped JS runtimes on top of .NET and the JVM. It's not a question of 'who writes the GC', it's more fundamental.
If you JIT your JavaScript down into raw native code that bangs rocks together to dereference pointers, you need to make sure your generated code handles pointers correctly. You need to make sure to get all your bounds checks right, etc.
Sure, the JVM could somehow have a 30-year-old bug in its array bounds checks. But if you're JITing javascript to an IR that doesn't have raw pointers and instead uses strongly-typed object references and bounds-checked arrays, you have automatically closed off a whole category of defects. At the point where you're saying "sure, but what if the JVM messes up array bounds checks?" you might as well be asking whether v8 can really afford to rely on read-only pages and guard pages for its security sandbox. What if the kernel is broken?
I mentioned type confusion attacks in particular because they're a class of attack that generally doesn't work against java or .net applications because values can't change type arbitrarily during execution. Local variables and parameters have known types, object type casts are checked, array elements are typechecked before being stored, etc. Obviously you pay a cost for this, and if you have threads the ABA problem rears its head, but JS is single-threaded by design.
Between hosting JS on the JVM or in WASM, WASM is probably a safer choice since it's such a constrained sandbox. But the JS runtime you're running inside of the WASM sandbox is still built in C, banging rocks together to dereference pointers. Hopefully you're running a modern security-hardened JS runtime inside that sandbox, and you haven't turned off all the security mitigations thanks to wasm's lack of page protections.
> People have previously shipped JS runtimes on top of .NET and the JVM. It's not a question of 'who writes the GC', it's more fundamental.
Yes. it is.
That's literally the whole point.
The bugs in this post are bugs in the runtime - the implementation of the Gc, the implementation of the object metadata.
If you build your JS engine on top of a safe/managed environment the attacker is not interested in attacking logic bugs in your JS engine, they're target the runtime. All you have done is move the problem from "the attacker exploited bugs in the JS runtime, how do we prevent those?" to "the attacker exploited bugs in the Java (or whatever) runtime, how do we prevent those?". The problem is that at some point any safe language (java, rust, or even - as here - javascript) has a runtime that has to be implemented in an unsafe environment, and that is what is being attacked.
The JVM and .NET are not magical, they have the same bugs - albeit with significantly less hardening and mitigations - as JS engines.
What you are saying is that the JS engine should be written in Java (or whatever) so it's safe. But now how do you fix the JVM? Maybe rewrite that in C#/.NET? But then you have to fix the .net VM? Maybe rust? of course then we need to ensure that's safe so we should run that all under wasm. Of course that means your back at the JS engine you started with.
There's a Java-in-Java implementation called GraalVM, if I'm not mistaken. So yes, if you are that worried about bugs in the JVM, you'd use a type-safe JVM too, and then compile your JS down to java bytecode.
V8 is a runtime for JS exactly like the JVM is a runtime for Java and CLR is for C#. Which means that whatever sandboxing V8 needs, the JVM and the CLR would need it as well. I don't know what makes you think that the JVM and the CLR have already solved the problem, but not V8.
That's called self-hosting, and it's widely used in JS runtimes to implement various built-ins instead of writing them in C++. It provides superior safety and the ability to inline builtins into their callers.
You didn't read the article, then. They clearly explain how even if Rust were used for the entirety of v8, there would still be memory corruption, because the memory corruption is happening in code that is JIT compiled.
I think they did because all the vulnerabilities in the hardening they talk about is because of C++ memory safety & would be fixed by Rust (i.e. their hardening technique doesn't target JIT exploits themselves).
No, it mentions that as an introduction, and then talks about the system for mitigating them, which also has bugs which they admit are of the simple kind that a memory-safe language would prevent.
No, this very much does help protect against JIT exploits.
JIT code contains code that accesses the data structures they are sandboxing. By sandboxing those objects, the JIT code is limited in what it can do.
This might help you understand: An example the article gives is if an optimization pass has a bug that forgets a check. Then it may emit JIT code that will access a data structure that it should not. But, thanks to this sandboxing, that object cannot be outside the sandbox, nor refer to anything outside the sandbox, so a JIT exploit is limited in what it can achieve.
My point was that the sandbox escape that is now required to exploit a JIT issue has nothing to do with JIT or things Rust won't help with. Indeed, the vast majority of sandbox escapes they've found are straight-up basic memory safety issues that Rust would protect against much better than trying to harden C++. Again, there's a real switching cost and 2nd system syndrome to consider, so I'm not saying "switch V8 to Rust" but ignoring that conversation wholesale is disingenuous, especially when it's a bait and switch (i.e. Rust doesn't help with JIT issues, here's this sandbox idea that does, except we wrote the sandbox in C++ & memory safety exploits in the sandbox/runtime are easily found & paired with the JIT exploit).
It’s interesting that it spends a lot of time talking about how memory safe languages don’t help V8 cause of JIT (true) & then talks about a hardening technique that does get helped by Rust. Why not just be honest and say that the switching cost to a new language is too expensive and error prone than play these games? Similarly the discussion about memory tagging - I know that it would harden the security of things like Cloudflare workers. And if most of the exploits are because it’s in-process, why not work on isolating it behind it’s own process (there must be ways to do this securely while not giving up too much performance)?
…because the overwhelming majority of memory safety bugs in js engines (v8, JSC, and spider monkey) are in operations that would be in unsafe blocks in rust as well?
In multiple decades I can think of a handful of engine bugs that would have been prevented by rust - and those were largely preventable (and now are) in c++ as well.
It is possible for rust to be a safer language than c++ and to also not meaningfully change the security profile of the language.
It’s not just the jit, the interpreter and GCs are also subject largely - necessarily - no more protected by rust than c++.
Did you read the article? It has nothing to do with the JIT. It uses the JIT as a smokescreen to talk about sandbox hardening & the issues within the sandbox are definitely not "unsafe" & 100% mitigated by Rust. Take a look at the relevant quotes I extracted in another comment to draw your attention to what the article is actually talking about (sandbox hardening).
The technique here keeps a large set of objects from escaping the sandbox. Those objects are accessed both by C++ and JIT code. You are right that using Rust instead of C++ would help on the C++ side, but it would not help at all on the JIT code side, and that is by far the major source of exploits.
In other words, even if you write a JS engine in Rust you could benefit greatly from this technique.
I have no issue with the sandbox technique. My critique is that the sandbox escapes just have to find an assumption violation in C++ that leads to a memory violation, meaning that sandbox escapes are easier in V8 due to being written in C++. Having the sandbox & security-relevant runtime pieces written in Rust would do a lot to prevent huge swathes of sandbox escapes more robustly than trying to ensure that all C++ today & that will be written remains hardened.
I don't think it's fair to call them dishonest here. It's pretty clear they've heard about memory safe languages, they've thought about it, they've considered in details the pros and cons.
>why not work on isolating it behind it’s own process (there must be ways to do this securely while not giving up too much performance)?
Well, you make it sound like the easy answer. A good exercise would be to try implementing what you're proposing in a comment. Not necessarily going all the way, but enough to know why it might not be as straightforward as you think.
The people working on V8 are not completely clueless, the concept of moving things out of process or using a memory safe language is not going to be a novel idea that they'll just start working on now that someone clever thought of it.
The dishonest piece is that the first part talks about why Rust doesn't help with the JIT (true) but then really talks about the V8 sandbox & hardening techniques they're applying to it where Rust would help 100%.
> However, assuming these numbers are simply stored as integers somewhere in the JSObject, an attacker could corrupt one of them to break this invariant. Subsequently, the access into the (out-of-sandbox) std::vector would go out of bounds. Adding an explicit bounds check, for example with an SBXCHECK, would fix this.
Or use Rust
> Encouragingly, nearly all "sandbox violations" discovered so far are like this: trivial (1st order) memory corruption bugs such as use-after-frees or out-of-bounds accesses due to lack of a bounds check
Or use Rust
> Contrary to the 2nd order vulnerabilities typically found in V8, these sandbox bugs could actually be prevented or mitigated by the approaches discussed earlier. In fact, the particular bug above would already be mitigated today due to Chrome's libc++ hardening
Or use Rust
I'm not saying rewrite the entire thing in Rust, too expensive & would introduce new bugs in the JIT for questionable benefit. But at least mention that & also discuss the technical challenges why the sandbox mechanism isn't written in Rust & what it would take to address those.
Look, I'm not saying the V8 team is making the wrong decisions. My questions are an indication of the shallowness of the blog write-up - why not explain some obvious questions that come up for someone who reads it?
The whole point is that the sandbox is an approach that can be used in JIT code, where Rust doesn't help.
Take the fizzbuzz example with a missing bounds check. Rust can't prevent you from generating JIT code that omits a bounds check on an array and reads/writes out-of-bounds. The sandbox doesn't prevent out-of-bounds reads/writes, but it guarantees that they will only be able to access data inside the sandbox.
This means that logic bugs in the JIT compiler are no longer immediately exploitable. They must be combined with bugs in the sandbox implementation. The article's claim is that, unlike compiler bugs, sandbox bugs tend to be amenable to standard mitigation techniques.
This article isn't dismissing the value of memory-safe languages. It's identifying a problem space where current memory-safe languages can't help, and providing an alternative solution. Currently, every browser JS engine is written in C++, in part because Rust doesn't solve the big correctness problems. If the sandbox approach works, then using Rust for other parts of the engine becomes more appealing.
To be clear. I have no issue with the sandboxing as a technique. It's perfectly valid and a good idea. My issue is that sandbox escapes are significantly easier than they should be due to the C++ runtime. Rust 100% would help mitigate sandbox escapes more effectively even as the codebase evolves. We know through lots of practical experience that "standard mitigation techniques" for C++ don't actually work all that well (there's at least about an order of magnitude difference in number of exploits possible between that & Rust).
How would rust help you when you're executing jitted code (ie assembly)? The fizzbuzz code would run in rust but the event handler would still be unsafe jitted code.
It doesn't: but it helps with the stuff around it. The article talks about 3 locations for bugs
1) jitted javascript code with subtle bugs due to logic errors in the compiler (Rust's memory safety can't really help here)
2) Bugs in surrounding utility code and the interpreter (Rust can help, but running without a JIT entirely is too slow. Still, it's part of the attack surface either way)
3) Bugs in the sandbox implementation which helps mitigate bugs of the first kind (Rust can help)
AFAIK the main objection raised here is the article dismisses moving to a memory safe language because it doesn't help with 1, but then discusses 2 and 3 where in fact the issues are exactly where memory safety can help.
> This code makes the (reasonable) assumption that the number of properties stored directly in a JSObject must be less than the total number of properties of that object. However, assuming these numbers are simply stored as integers somewhere in the JSObject, an attacker could corrupt one of them to break this invariant. Subsequently, the access into the (out-of-sandbox) std::vector would go out of bounds. Adding an explicit bounds check, for example with an SBXCHECK, would fix this.
> Encouragingly, nearly all "sandbox violations" discovered so far are like this: trivial (1st order) memory corruption bugs such as use-after-frees or out-of-bounds accesses due to lack of a bounds check. Contrary to the 2nd order vulnerabilities typically found in V8, these sandbox bugs could actually be prevented or mitigated by the approaches discussed earlier. In fact, the particular bug above would already be mitigated today due to Chrome's libc++ hardening. As such, the hope is that in the long run, the sandbox becomes a more defensible security boundary than V8 itself
It's still not clear to me what Rust feature would prevent what specific vulnerability here. Rust has bounds-checked and non-bounds-checked array accesses depending on the developer's preference, and so does C++. If there's some point you're making with these quotes you're going to need to simplify it for me since I'm not following.
> Rust has bounds-checked and non-bounds-checked array accesses depending on the developer's preference, and so does C++
You're making it sound like these are the same, the difference is the defaults
Unsafely accessing an element in C++
vec[i]
Unsafely accessing an element in Rust
unsafe { vec.get_unchecked(i) }
One of these is screamingly obvious that something potentially unsafe is happening and should be audited more closely, that's the real difference. The cause of potential memory issues is isolated and searchable in `unsafe` blocks rather than being potentially anywhere
Is safe in v8, blink, jsc, webkit, etc. Rust has a huge number of benefits over c++, but it hurts your argument if you refuse to acknowledge the actual environment the C++ is being used in and make objectively incorrect statements. It implies a lack of understanding of C++ and sounds like all you're doing is parroting other people's critiques without understanding the core issues, which undermines your message.
That said it's still not particularly relevant here, because the issues being presented are bugs in the runtime. e.g. the runtime logic and state results in erroneous behavior. The bugs being discussed are not "you did not use a safe vec" or "you did not use Rc", it's "the size or bounds check in vec is incorrect" or "the ref counting in Rc is incorrect". Rust does not inherently stop those the runtime from having bugs, it simply statically limits where the exposure to unsafe operations can occur.
That's super relevant to program safety, but it's not relevant to safety in the JS VM runtime, where they're performing the operations that would be unsafe{} in rust as well.
So the Rust feature is "screaming obviousness"? Your argument is that the advantage of rewriting the module in Rust is that finding array accesses is visually easier?
It's not just that. It's that you can also add `#![forbid(unsafe_code)]` to your crate. Now you know there's 0 potential memory safety issues of any kind.
Keep in mind that failed bounds checks are only 1 memory safety possibility for C++. Another common one is UB behavior which abounds all over the place in C++.
The only language security issue you have to worry about with safe Rust is that integers overflow by default in release mode which means you need to explicitly annotate summations that could cause a security exploit with `saturating_X`, `checked_add`, or `wrapping_add` depending on what behavior you want when you exceed the integer range bounds. In cases where security by default is more important, setting `overflow-checks` to `true` in the Cargo profile is better - that way you have to explicitly opt-in to wrapping behavior if you want more performance in the hot path.
If you're willing to take the performance hit, Chromium actually allows you to disable JIT easily in the Settings and add exceptions for certain sites. Open the Settings and search for V8 Optimiser.