// 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.
```js
let array = new Array(100);
let evil = { [Symbol.toPrimitive]() { array.length = 1; return 15; } };
array.push(evil);
// 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?