1 of 60

Blink-in-JavaScript

Kentaro Hara (haraken@chromium.org)

2 of 60

What is it?

- Blink-in-JavaScript is a mechanism to enable Blink developers to implement DOM features in JavaScript (instead of C++)

3 of 60

Team

- haraken@

- tasak@

- yosin@

- yoicho@

- jochen@

- dcarney@

4 of 60

Agenda

- Concept

- Design

- Implementation

- The main issue is how to ensure security

5 of 60

Concept

6 of 60

Motivation

- C++ causes a lot of security bugs

- C++ is hard to maintain

- If we can implement more things in JS, we can make Blink more secure and easier to maintain

7 of 60

The basic idea

- Implement only the core part in C++

- Implement other parts in JS on top of existing, web-exposed JS APIs

8 of 60

Targets of Blink-in-JS

- High-level DOM features that can be easily implemented on top of existing JS APIs

- DOM features that are unloved and should be factored out from C++

- DOM features that are going to be deprecated

- DOM features that are going to be implemented in C++ in the near future (i.e., Polyfil)

9 of 60

Targets of Blink-in-JS

- Examples:

- XSLT

- Editing’s execCommand()

- A bunch of editing APIs

- ScriptRegexp

- Node.normalize()

- DOMWindow.atob()/btoa()

- ...

10 of 60

Example: XSLT

- XSLT adds a lot of complexity to the code base

- We do want to remove it, but can’t because of non-negligible number of users (in enterprise area)

- So let’s factor it out from C++ to JS!

11 of 60

Example: Editing APIs

- Editing APIs have a ton of use-after-free bugs

- Editing APIs can be implemented on top of existing JS APIs

- Most of them are not performance-sensitive

- So let’s move it to JS :)

12 of 60

Summary

- The goal of Blink-in-JS is improving:

- maintainability

- security

- layering of the web architecture

- The goal of Blink-in-JS is NOT improving:

- performance

- power

- memory

13 of 60

Better maintainability

- Maintainability matters

- Simplifying the code base allows us:

- to make performance improvements

- to add more important features more quickly

14 of 60

Better layering of the web architecture

- Better layering improves security

- Currently we implement everything in C++, so we need to ensure security for everything…

- If we implement only the core part in C++ and other parts in JS, we just need to ensure security for the C++ part and the JS engine

15 of 60

Wait!

- What about performance/power/memory? Won’t they regress?

16 of 60

Performance & power

- Problem:

- JS is slower than C++ (and thus consumes more power)

- Solution:

- Performance-sensitive features are not the target of Blink-in-JS

17 of 60

Memory

- Problem:

- JITed JS code is 20x~ larger than C++ binary

- Solution:

- Blink-in-JS is lazily complied (it’s not compiled until the feature is requested)

- The compiled code is discardable anytime (the code is recompiled when the feature is requested again)

18 of 60

Summary

- Blink-in-JS enables Blink developers to implement DOM features in JS

- The goal is to improve:

- maintainability

- security

- layering of the web architecture

19 of 60

Design

20 of 60

Programming model

- It’s easy; you just need to:

- add [ImplementedInJS] to DOM attributes/methods in IDL files

- implement the DOM attributes/methods in JS

- Then, necessary binding code will be auto-generated

21 of 60

Programming model

// WindowBase64.idl

interface WindowBase64 {

[ImplementedInJS] DOMString atob(DOMString str);

};

// WindowBase64.js

installClass(“WindowBase64”, function() {

return {atob: function atob(str) {

// Here |this| is equal to |window|.

return base64Encode(str);

}};

});

22 of 60

Notes

- It’s also possible to use Blink-in-JS (not through IDL but) from inside Blink

- What Blink-in-JS can use is limited to web-exposed JS APIs

- Future work: Expose internal APIs that are visible only to Blink-in-JS

23 of 60

How it works

- Blink-in-JS is lazily compiled at the first time the DOM attribute/method is accessed

- Blink-in-JS is executed in the same security level as Chrome extensions

24 of 60

Security model

- The problem is that we cannot execute Blink-in-JS in the same “world” (explained later) as user’s JS

- ...because Blink can have confidential information that should not be exposed to user’s JS

- File names in an <input> element

- Contents of a clipboard

25 of 60

Security model

- Requirements:

- Blink-in-JS and user’s JS need to operate the same C++ DOM objects

- However, JS objects should not leak between Blink-in-JS and user’s JS

- In short, underlying C++ DOM objects should be shared between Blink-in-JS and user’s JS, but JS objects should be isolated

26 of 60

Security model

- C++ DOM objects are shared, but their DOM wrappers are separated

- ...and thus guarantees

that no JS objects leak

between Blink-in-JS and

user’s JS

27 of 60

Security model

- This is exactly what Chrome extensions are doing

- using a concept of “world” (explained later)

- So Blink-in-JS uses the same infrastructure and guarantees the same level of JS isolation

- Blink-in-JS is “a Chrome extension inside Blink”

- Blink-in-JS switches the world whenever it is entered/exited

28 of 60

Summary

- It’s easy to use Blink-in-JS

- Blink-in-JS is lazily compiled

- Blink-in-JS is executed in the same security level as Chrome extensions

29 of 60

Implementation

(Mostly about how to ensure security)

30 of 60

I mentioned...

- Chrome extensions guarantee security using a concept of “world”

- Blink-in-JS uses the same infrastructure and guarantees the same level of JS isolation as Chrome extensions

31 of 60

However...

- The problem is that the implementation of the “world” is broken :-/

- JS objects sometimes leak among worlds...

- We must fix it; it’s not only for Blink-in-JS but also for all Chrome extensions

32 of 60

What’s the problem?

- To understand the problem, you need to understand complicated concepts in V8 bindings:

- Isolate

- Context

- World

- I will explain these now :)

33 of 60

Isolate

- An isolate is a V8 concept, associated to each thread

- One isolate is for the main thread

- One isolate is for each worker thread

34 of 60

Context

- A context is a V8 concept, associated to a global variable scope

- Roughly speaking, a context corresponds to a window

- Each frame has its own window and thus its own context

- e.g., window.foo in an <iframe> is different from window.foo in another <iframe>

35 of 60

World

- A world is a concept to sandbox DOM wrappers among content scripts of Chrome extensions

36 of 60

World

- In one isolate:

- underlying C++ DOM objects are shared among worlds

- but the DOM wrappers are separated

- Each world has its own context

- e.g., Object.prototype is different per world

- Therefore, it is guaranteed that no JS objects leak among worlds

37 of 60

World

- A world is a concept to completely sandbox JS executions except underlying C++ DOM objects

- The current problem is that DOM wrappers can leak among worlds (and thus JS objects can leak among worlds)

- e.g., A world can access a window object of another world...

38 of 60

Isolate, context, world

- Isolate = Thread

- Context = Global scope (window object)

- World = Content script

39 of 60

Isolate, context, world

- Remember that:

- Each frame has its own context

- Each world has its own context

- This means that if one isolate has x frames and y worlds, there are x*y contexts involved

40 of 60

Isolate, context, world

- One global scope is needed for each pair of (page frame, content script)

41 of 60

Isolate, context, world

- Whenever you access DOM wrappers (e.g., when you call toV8()), you need to make sure that you are in a correct context

- Otherwise, you will end up returning DOM wrappers of another world, which will lead to cross-world leakage

42 of 60

// main.html

<iframe src=”iframe.html”></iframe><script>

var iframe = document.querySelector(“iframe”);

iframe.onload = function () {

var div = iframe.contentDocument.querySelector(“div”); // The <div> wrapper should be created in the context associated with the main frame and the current world

div.onclick = function() { ... } /* This should be invoked in the context that registered the event handler */

div.click();

}

</script>

// iframe.html

<div></div><script>

var div = document.querySelector(“div”); // The <div> wrapper should be created in the context associated with <iframe> and the current world

div.onclick = function() { ... } /* This should be invoked in the context that registered the event handler */

</script>

43 of 60

Anyway, you must be in a correct context

(1) When the event handler is created, you need to get the current context and record it

(2) When the event handler is invoked (sometime later), you need to restore the context, and then invoke the event handler

44 of 60

Revisited: What’s the problem?

- Isolate, context and world are complicated

- People write binding code without understanding it

- People tend to use a current context when they don’t know what context they should use

- The current context is not always equal to a correct context

- It can lead to cross-world leakage...

45 of 60

Solutions

Solution 1: Invent a better programming model everyone can understand

Solution 2: Introduce dynamic verifications about cross-world leakage

46 of 60

Solution 1: Better programming model

- There are two cases where binding code is executed

- Synchronous case: JS calls the binding code and immediately go back to JS

- e.g., div.firstChild, div.appendChild()

- Asynchronous case: JS calls the binding code and creates some proxy object, and then later Blink calls back the binding code through the proxy object

- e.g., Event handlers, Promise

47 of 60

Solution 1: Better programming model

- The synchronous case is no problem

- Because JS is calling you, it’s already guaranteed that you are in a correct context

48 of 60

Solution 1: Better programming model

- The asynchronous case needs special handling

- The basic idea is:

(1) When JS calls the binding code and creates a proxy object (e.g., V8EventListener), store the current context

(2) When later Blink calls back the binding code through the proxy object (e.g., V8EventListener::handleEvent()), restore the context before accessing DOM wrappers

49 of 60

Solution 1: Better programming model

class V8ProxyObject { // e.g., V8EventListener

V8ProxyObject() : m_state(ScriptState::current()) { }

void someCallback() { // Blink calls back later

if (m_state->contextIsEmpty()) // Context is already gone

return;

ScriptState::Scope scope(m_state.get()); // Enter the context

...;

}

RefPtr<ScriptState> m_state; // ScriptState piggybacks isolate, context, world and all other information about script execution

};

50 of 60

Solution 2: Dynamic verifications

- Introducing ScriptState will fix cross-world leakage

- As a next step, it’s important to verify

- Specifically, we’re going to use:

- ScriptValue

- Security tokens

51 of 60

Solution 2: Dynamic verifications

- ScriptValue is a thin wrapper of a V8 value

- When a Blink object holds a V8 value, ScriptValue should be used

class V8EventListener {

ScriptValue m_listenerFunction;

};

52 of 60

Solution 2: Dynamic verifications

- Verify that ScriptValue is always accessed from the world from which the ScriptValue is created

- By doing this, we can verify that no V8 values held by Blink objects leak among worlds

class V8EventListener {

ScriptValue m_listenerFunction;

};

53 of 60

Solution 2: Dynamic verifications

- A security token is a V8 concept to detect cross-context access

- A context can have a security token

- If a JS object accesses another JS object created from a context that has a different security token, V8 detects the error

x = ...; // An object from one context

y = ...; // An object from another context that has a different security token

x.foo = y; // V8 detects the error and sets undefined

54 of 60

Solution 2: Dynamic verifications

- If we set the same security token on all contexts in the same world, V8 detects all cross-world leakage for us

x = document.xxx(); // xxx() returns a wrapper of one world

y = document.yyy(); // Assume that yyy() is mis-implemented and returns a wrapper of another world

x.foo = y; // V8 detects the error and sets undefined

55 of 60

Solution 2: Dynamic verifications

- The security token is a perfect way to detect cross-world leakage

- The problem is that the current implementation is not yet perfect

- We’re making it perfect :)

56 of 60

Summary

- We need to guarantee that no DOM wrappers leak among worlds

- This is not only for Blink-in-JS but also for all Chrome extensions

- We are fixing it by:

- inventing a better programming model with ScriptState

- introducing dynamic verifications

57 of 60

Conclusion

58 of 60

Conclusion

- Blink-in-JS enables developers to implement DOM features in JS

- The goal is to improve security, maintainability and layering of the web architecture

- The challenging part is to eliminate all cross-world leakage

59 of 60

Working plan

(1) Refactor confusing infrastructures about isolate, context and world

(2) Introduce ScriptState to the code base and fix all cross-world leakage

(3) Implement dynamic verifications about cross-world leakage

(4) Land the infrastructure of Blink-in-JS

(5) Move XSLT and editing/ to Blink-in-JS

- Now we’re working on (2) and (3)

60 of 60

Thanks!