JavaScript in JavaScript (js.js): Sandboxing Third-Party Scripts

Early last fall, I started working on a project called js.js with two other graduate students, Naga Katta and Stephen Beard. We started using a public Github repository from the start, and at the beginning of January, the author of three.js found and tweeted a link to our repository to his many followers. Soon after, a post wound up on Hacker News. Unfortunately, we weren’t very far along in the project yet, and weren’t really ready to show anyone our work, so there was a lot of confusion and negative criticism.

We’ve reached the point where js.js performs reasonably well and has a decent amount of functionality implemented. In this post, I’ll outline what js.js is, how it works, demonstrate a sample application that uses it, and show results of a performance analysis.

js.js is a JavaScript interpreter (which runs in JavaScript) that allows an application to execute a third-party script inside a completely isolated, sandboxed environment. An application can, at runtime, create and interact with the objects, properties, and methods available from within the sandboxed environment, giving it complete control over the third-party script. js.js supports the full range of the JavaScript language, is compatible with major browsers, and is resilient to attacks from malicious scripts.

Our initial prototype implementation of the js.js runtime has been created by compiling the SpiderMonkey JavaScript interpreter to LLVM bytecode using the Clang compiler and then using Emscripten to translate the LLVM bytecode to JavaScript.

Emscripten

Emscripten, a project by Alon Zakai, is an LLVM-to-JavaScript compiler. It takes LLVM bitcode (compiled with an LLVM frontend like Clang) and compiles that into JavaScript, which can be run on the web. There is some great technical documentation on how this works on the Emscripten wiki, so I won’t go into too much detail, but I’ll give an example of this process.

Here’s a simple C++ functions that calculates a Fibonacci number:

int fibonacci(unsigned int n) {
  if (n==0 || n==1) {
    return n;
  }
  unsigned int prev2 = 0, prev1 = 1, fib = 1, i;
  for (i=2; i<=n; i++) {
    fib = prev1 + prev2;
    prev2 = prev1;
    prev1 = fib;
  }
  return fib;
}

After compiling this with Clang, below is the LLVM bitcode:

define i32 @fibonacci(i32 %n) nounwind readnone {
  %1 = icmp ult i32 %n, 2
  br i1 %1, label %.loopexit, label %.lr.ph

.lr.ph:                                           ; preds = %.lr.ph, %0
  %i.04 = phi i32 [ %3, %.lr.ph ], [ 2, %0 ]
  %prev2.03 = phi i32 [ %fib.02, %.lr.ph ], [ 0, %0 ]
  %fib.02 = phi i32 [ %2, %.lr.ph ], [ 1, %0 ]
  %2 = add i32 %prev2.03, %fib.02
  %3 = add i32 %i.04, 1
  %4 = icmp ugt i32 %3, %n
  br i1 %4, label %.loopexit, label %.lr.ph

.loopexit:                                        ; preds = %.lr.ph, %0
  %.0 = phi i32 [ %n, %0 ], [ %2, %.lr.ph ]
  ret i32 %.0
}

This is where Emscripten comes in. It takes this LLVM bitcode and generates JavaScript instructions. Rather than trying to emulate the LLVM, it actually translates operations that it can into their JavaScript equivalent. Here’s what it looks like after translation:

Module._fibonacci = (function (a) {
    var b = 2 > a;
    a: do {
        if (b) {
            var d = a
        } else {
            for (var c = 1, e = 0, f = 2;;) {
                var k = e + c,
                    f = f + 1;
                if (f > a) {
                    d = k;
                    break a
                }
                e = c;
                c = k
            }
        }
    } while (0);
    return d
});

To see this working in actions, here’s a jsfiddle showing this function calculating the 20th number in the Fibonacci sequence. Notice an important thing happening in this translation: the LLVM bitcode is not getting emulated. It has actually translated addition, assignment, and comparison operators into their equivalent JavaScript form.

js.js

To create js.js, we ran Emscripten on SpiderMonkey, the JavaScript engine used in Firefox. SpiderMonkey comprises about 300,000 lines of C and C++ code. Much of our effort was spent patching SpiderMonkey to get it to compile in Emscripten’s environment, a limited subset of libc. We also had to disable all assembly routines and just-in-time (JIT) compiling features of SpiderMonkey, since assembler is not available in JavaScript.

Once we had the SpiderMonkey API available, we wrote a wrapper script that makes it much easier to use the library from JavaScript. The js.js API wrapper is about 1000 lines of JavaScript and allows you to create sandboxed environments and execute code in them. The following is an example that shows how to use the API to run 1+1 in a sandbox and get the result as a number:

var src = "1 + 1";
var jsObjs = JSJS.Init();
var compiledObj = JSJS.CompileScript(jsObjs.cx, jsObjs.glob,
                                     src);
var rval = JSJS.ExecuteScript(jsObjs.cx, jsObjs.glob,
                              compiledObj);
d = JSJS.ValueToNumber(jsObjs.cx, rval);

After executing, the value of d will be 2.

Performance Evaluation

We wanted to quantify the performance overhead at the microbenchmark level and the macrobenchmark level. For the former, we timed each of the js.js functions, and for the latter, we used the SunSpider JavaScript benchmark.

Microbenchmark

To quantify the performance of each the js.js API function, the following table shows the mean across 10 runs of execution time (in milliseconds) of each function called by the above 1+1 example:

Operation Mean Execution Time (ms)
libjs.min.js load 84.9
NewRuntime 25.2
NewContext 35.8
GlobalClassInit 15.5
StandardClassesInit 60.1
Execute 1+1 70.6
DestroyContext 33.3
DestroyRuntime 1.8

The execution time here isn’t great, but it’s within the range that the library is usable. It takes about 220ms of setup time to get an execution environment up and running.

Macrobenchmark

We also wanted to compare the performance of js.js with native JavaScript execution. We took the SunSpider JavaScript benchmark and ran it natively using the SpiderMonkey js shell (with the JIT turned off). We then ran the benchmarks again using js.js. The following graph shows the median factor of slowdown for running each benchmark inside js.js in both Firefox and Chrome:

js.js Sunspider Benchmark


This benchmark shows that on average, running code inside js.js is about 200 times slower than native execution. Considering that JavaScript is being run instead of native x86, two orders of magnitude is not terrible. Depending on the scripts being executed in the sandbox, the performance overhead might be acceptable. We’ve also looked into where most of the execution time is going, and we found that the interpreter loop is being converted into a single JavaScript function that is thousands of lines long. JIT compilers don’t handle this case very well, so we’ve been brainstorming ideas on how to break up this interpreter loop into separate functions, that could help improve performance.

Demo

As a demo, we took the JavaScript used to render the Twitter button and ran it inside js.js, giving the script virtual access to a DOM. This allows us to run the script, while maintaining complete control over what the sandbox can do to the real DOM.

Live js.js Twitter Demo

More Demos

Conclusion

This project has been a lot of fun to work, and we recently had a demo paper about js.js accepted to WebApps 2012. For more details, check out the js.js paper. The source code for js.js is available on GitHub, so if you’re interested in the project, you’re welcome to fork it and try it out. Pull requests accepted!

  • Nabil2387

     hello, I tried to install it in the end I install llvm and clang, but I am not come installed emscripten, I have an error saying, Node.Js Clang does not know, can we commit for configuer node so it can clang know, I do these two statements: ”   clang tests/hello_world.cpp
    ./a.out “, and when I do that : node hello_world, it gives me this erro : :rmodule.js:340: ” module.js:340 throw err; ^Error: Cannot find module ‘/home/benamara/c-build/llvm-3.1.src/test/hello.js’ at Function.Module._resolveFilename (module.js:338:15) at Function.Module._load (module.js:280:25) at Module.runMain (module.js:487:10) at process.startup.processNextTick.process._tickCallback (node.js:244:9)try to explain it to me , pliiz !!

  • http://www.cs.princeton.edu/~jterrace/ Jeff Terrace

    Can you please open an issue on GitHub?

  • Nabil2387

    look, me my project is to compile language on the web, then I am employed on linux, I used this link: I think I have a problem with the Node, if it’s possible that you pass me tn your msn facebook or who personally contact you, and thank you very much!

  • http://www.cs.princeton.edu/~jterrace/ Jeff Terrace

    Email me at jterrace@gmail.com or file an issue at https://github.com/jterrace/js.js/issues/new

  • http://www.makemyriff.com/ larrybattle

    True that!

  • ukemarketing

    Yeah the execution time is the only problem for me, but looking at it I’m sure there could be a way to shave a little time off.

  • Blago Stroy

    Хорошая статья, именно так нужно покупать строительные материалы в Одессе.

  • http://www.ayaonline.com/ Aya International

     Yup! yu made it.

  • http://lizyd.co.za/ Web Design

    Ama zinngg :D

  • Ferdinand Pročko
  • http://twitter.com/Muse_Event Muse Event Planning

    Nice code

  • http://www.facebook.com/sair.ertunc.demiriz Ertunç Demiriz

    yes.

  • Mehmet
  • sameer

    Thanks , This Is A Great Post
    Virgintech

  • sameer

    Great Post
    Please Visit http://www.virgintech.in