Design of V8 bindings

This document explains key concepts in the V8 binding architecture except the lifetime management of DOM wrappers. See V8GCController.md to learn the lifetime management.

Isolate

An isolate is a concept of an instance in V8. In Blink, isolates and threads are in 1:1 relationship. One isolate is associated with the main thread. One isolate is associated with one worker thread. An exception is a compositor worker where one isolate is shared by multiple compositor workers.

Context

A context is a concept of a global variable scope in V8. Roughly speaking, one window object corresponds to one context. For example, <iframe> has a window object different from a window object of its parent frame. So the context of the <iframe> is different from the context of the parent frame. Since these contexts create their own global variable scopes, global variables and prototype chains of the <iframe> are isolated from the ones of the parent frame.

Here is an example:

// main.html
<html><body>
<iframe src="iframe.html"></iframe>
<script>
var foo = 1234;
String.prototype.substr =
    function (position, length) { // Hijacks String.prototype.substr
        console.log(length);
        return "hijacked";
    };
</script>
</body></html>

// iframe.html
<script>
console.log(foo);  // undefined
var bar = "aaaa".substr(0, 2);  // Nothing is logged.
console.log(bar);  // "aa"
</script>

In summary, each frame has a window object. Each window object has a context. Each context has its own global variable scope and prototype chains.

Entered context and current context

A relationship between isolates and contexts is interesting. One isolate has to execute JavaScripts in multiple frames, each of which has its own context. This means that the context associated with the isolate changes over time. In other words, the relationship between isolates and contexts is 1:N over the lifetime of the isolate.

Here we have a concept of an entered context and a current context. To understand the difference, you need to understand two kinds of runtime stacks.

The first stack is a stack of JavaScript functions. This stack is managed by V8. When one function calls another function, the callee function is pushed onto the stack. When that function returns, the function is popped from the stack and the control returns to the caller function that is now on the top of the stack. Each function has an associated context. We call the context of the function that is currently running (i.e., the context of the function that is on the top of the stack) a current context.

Here is an example:

// main.html
<html><body>
<iframe src="iframe.html"></iframe>
<script>
var iframe = document.querySelector("iframe");
iframe.onload = function () {
    iframe.contentWindow.func();
}
</script>
</body></html>

// iframe.html
<script>
function func() {
  ...;
}
</script>

In the above example, at the point when func() is running, the current context is the context of the <iframe>.

There is a second stack that operates on a much coarser granularity. This stack is managed by V8 binding (not by V8). When V8 binding invokes JavaScript, V8 binding enters a context and pushes the context onto the stack. The JavaScript starts running on the context. When the JavaScript finishes and the control returns back to V8 binding, V8 binding pops the context from the stack. Given that the control between V8 binding and V8 can be nested (i.e., V8 binding invokes JavaScript, which calls into V8 binding, which invokes another JavaScript etc), these contexts form a stack. The pushing and popping are done by any V8 API that takes a context argument or by explicitly calling v8::Context::Enter() and v8::Context::Exit(). We call the most recently entered context an entered context.

In the above example, at the point when func() is running, the entered context is the context of the main frame (not the context of <iframe>).

The entered context is a concept to implement the entry settings object of the HTML spec. The current context is a concept to implement the incumbent settings object of the HTML spec.

In summary, the entered context is a context from which the current JavaScript execution was started. The current context is a context of the JavaScript function that is currently running.

There is another special context called a debugger context. If a debugger is active, the debugger context may be inserted to the context stack.

World

A world is a concept to sandbox DOM wrappers among content scripts of Chrome extensions. There are three kinds of worlds: a main world, an isolated world and a worker world. A main world is a world where a normal JavaScript downloaded from the web is executed. An isolated world is a world where a content script of a Chrome extension is executed. An isolate of the main thread has 1 main world and N isolated worlds. An isolate of a worker thread has 1 worker world and 0 isolated world. This diagram will be helpful to understand the relationship.

All worlds in one isolate share underlying C++ DOM objects, but each world has its own DOM wrappers. That way the worlds in one isolate can operate on the same C++ DOM object without sharing any DOM wrapper among the worlds.

Also each world has its own context. This means that each world has its own global variable scope and prototype chains.

As a result of the sandboxing, the worlds in one isolate cannot share any DOM wrappers or contexts but can share underlying C++ DOM objects. The fact that no DOM wrappers or contexts are shared means that no JavaScript objects are shared among the worlds. That way we guarantee the security model that Chrome extensions doesn't share any JavaScript objects while sharing the underlying C++ DOM objects. This sandbox allows the Chrome extensions to run untrusted JavaScripts on a shared DOM structure.

(Note: An isolated world is a concept of V8 binding, whereas an isolate and a context are a concept of V8. V8 does not know what isolated worlds are in an isolate.)

In summary, an isolate of the main thread consists of 1 main world and N isolated worlds. An isolate of a worker thread consists of 1 worker world and 0 isolated world. All worlds in one isolate share the underlying C++ DOM objects, but each world has its own DOM wrappers. Each world has its own context and thus has its own global variable scope and prototype chains.

A relationship between isolates, contexts, worlds and frames

Let's wrap up the relationship between isolates, contexts, worlds and frames.

  • As a requirement of the DOM side, one HTML page has N frames. Each frame has its own context.

  • As a requirement of the JavaScript side, one isolate has M worlds. Each world has its own context.

As a result, when we execute the main thread where N frames and M worlds are involved, there exists N * M contexts. In other words, one context is created for each pair of (frame, world). This diagram will be helpful to understand the relationship.

The main thread can have only one current context at one time, but the main thread can have the N * M contexts over its lifetime. For example, when the main thread is operating on a frame X using a JavaScript in a world Y, the current context is set to a context for the pair of (X, Y). The current context of the main thread changes over its lifetime.

On the other hand, a worker thread has 0 frame and 1 world. Thus a worker thread has only 1 context. The current context of the worker thread never changes.

DOM wrappers and worlds

For compatibility reasons, we need to make sure that the same DOM wrapper is returned to JavaScript as long as the underlying C++ DOM object is alive. We should not return different DOM wrappers for the same C++ DOM object.

Here is an example:

var div = document.createElement("div");
div.foo = 1234;  // expando
var p = document.createElement("p");
p.appendChild(div);
div = null;
gc();
console.log(p.firstChild.foo);  // This should be 1234, not undefined

To accomplish the semantics that the same DOM wrapper is returned to JavaScript as long as the underlying C++ DOM object is alive, we need a mapping from the C++ DOM objects to the DOM wrappers. In addition, we need to sandbox DOM wrappers in each world. To meet the requirements, we make each world hold a DOM wrapper storage that stores a mapping from the C++ DOM objects to the DOM wrappers in that world.

As a result, we have multiple DOM wrapper storages in one isolate. The mapping of the main world is written in ScriptWrappable. If ScriptWrappable::main_world_wrapper_ has a non-empty value, it is a DOM wrapper of the C++ DOM object of the main world. The mapping of other worlds are written in DOMDataStore.

DOM wrappers and contexts

When you create a new DOM wrapper, you need to choose a correct context on which the DOM wrapper is created. If you create a new DOM wrapper in a wrong context, you will end up with leaking JavaScript objects to other contexts, which is very likely to cause security issues.

Here is an example:

// main.html
<html><body>
<iframe src="iframe.html"></iframe>
<script>
var iframe = document.querySelector("iframe");
iframe;  // The wrapper of the iframe should be created in the context of the main frame.
iframe.contentDocument;  // The wrapper of the document should be created in the context of the iframe.
iframe.contentDocument.addEventListener("click",
    function (event) {  // The wrapper of the event should be created in the context of the iframe.
        event.target;
    });
</script>
</body></html>

// iframe.html
<script>
</script>

To make sure that a DOM wrapper is created in a correct context, you need to make sure that the current context must be set to the correct context whenever you call ToV8(). If you're not sure what context to use, ask haraken@chromium.org.