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!

  • Y U NO COMPARE TO NARCISSUS?

  • That’s a great idea – I might try that.

  • This is a very exciting development! Hopefully the community will band together to bring down execution time.

  • Using Chrome-18 I got:

    === GETTING === data-twttr-rendered === null 
    Uncaught wrong value!

  • Guesy

    so what implications does this development have for a end user?

  • Hmm, does it consistently say that? I can’t reproduce that here.

  • Yup,I even disabled all my incognito extensions and opened an incognito window and it continued to occur.
    —–
    Navarr T. Barnier
    navarr@gtaero.net
    http://navarr.me/

  • eagspoo

    f-ing awesome

  • Guest

    How do allow the Javascript access to useful APIs?  Why did you write your own interpreter instead of taking a verifier approach like ADSafe or a source code rewriter approach like Caja?

  • If you check out the paper, we have a related work section where we discuss ADsafe and Caja.

  • Pingback: JavaScript in Javascript | No more cubes.()

  • Guest

    It sounds great, but, it a little bit complicated for a very simple concept in Javascript, is it not?
    What about simply redefining symbols that should not be accessed?

  • wewebuk

    Nice, I would like to see more regarding this, but also It can be achieved with much simpler code

  • Erik Corry

    It seems there is no comparison to the NaCl port of V8.

    Also, startup times for the pages containing js.js would be interesting.

  • Guénolé Marquier

    Until 46 as iteration number the Fib demo works fine

    But starting from 47 the native and js.js buttons give different results :(

  • This is because I’m converting to an i32 rather than a double. It should be able to be fixed relatively easily.

  • Hmm, it would be interesting to do, but the NaCl client version should win by a landslide. It’s not really a fair comparison.

    As for startup time – that’s what the first entry is in the table of microbenchmark times.

  • PIT

    Wow, It’s really amazing!

  • I’m consistently get this error too.

  • larrybattle

    Would there be an performance increase if you used web workers?

  • Not really – the interpreter is single-threaded. It potentially could be put it in a background web worker so it wouldn’t block the main thread, but DOM access would be difficult from a web worker.

  • ZJM

    I think there may be an issue with the Fibonacci demo
    in js.js for numbers larger than 46. Overflow? Using Chrome 18.

  • Yes, see my reply to Marquier’s comment about this

  • Irishado

    It’s not about if it will work its about trying to do something that no one thought about.

    Excellent work guys keep it up

  • Andrew Fuchs

    Jeff, this is some great code!

  • Very good work!

  • Colin Faulkingham

    yo dawg. I heard you like javascript. So we put Javascript in your Javascript so you can write Javascript for your Javascript. :-)

  • ThomasX

    Could someone do this for PHP please?

  • Guest

    FYI:

    In Fibonacci, Chrome gives me

    js.js result for iteration 100: 1990706582
    Native result for iteration 100: 354224848179262000000

  • Erik Corry

    So it’s over 300ms of pure CPU time to do a hello world (or 1+1) startup-work-shutdown cycle.  That’s pretty heavy.  This is on a beefy laptop machine, and not a phone or netbook.

    What’s the justification for switching off the JIT in the native version when doing the speed comparison?  Seems like you are just arbitrarily hobbling the competition there.

    What I learn from this is that there are good engineering reasons to prefer the approach of restricting the dialect of JS that you support as some of the alternative approaches do.  Perhaps one should go even further and require the to-be-sandboxed code to do without eval.

  • Switching off the JIT ensures that we have a fair comparison. We wanted to measure the overhead of running the interpreter in JavaScript. There are plenty of benchmarks showing how good the JIT performs, so it’s not really interesting to compare to a JIT version.

  • Pingback: Weekly HTML5 Apps Developer Resources, April 25th 2012 ✩ Mozilla Hacks – the Web developer blog()

  • Uhnn, .js in .jsA little way over my head :) Hope my brain will be able to parse or compile that int bytecode…. *yawn*

    Good job fellas lol

  • Yansky

    Which version of javascript does js.js support?

  • Everything that SpiderMonkey supports, which is all of them from 1.0 to 1.8

  • Pingback: [patagonia sale]()

  • Pingback: windows tweaks()

  • Ercan inan

    Which version of javascript does js.js support?http://www.denizinkalbi.com

  • Aa

    It’s great. 

  • Trackstar Web Design

    Doesn’t work for me:(

  • Amazing piece of coding and results.

  • webmaster sweetalma

    WOW!, Thanks for the nice Blog. This is really Fantastic. Enjoy online fashion shopping with us! We are- Kuku fashion Australia, with Kuku, Kuku dress, kuku dresses, Kuku fashion, Mad Love stockist, mad love fashion, Toi et Moi online, Toi et Moi jacket. Thanks for staying with us.
     

  • webmaster sweetalma

    WOW!, Thanks for the nice Blog. This is really Fantastic. Enjoy online fashion shopping with us! We are- Kuku fashion Australia, with Kuku, Kuku dress, kuku dresses, Kuku fashion, Mad Love stockist, mad love fashion, Toi et Moi online, Toi et Moi jacket. Thanks for staying with us.
     

  • nice, useful

    thanks

  • Informative post,thanks..

  • you article is nice anybody get help for it
    http://www.bogra-habib.blogspot.com

  • which version of javascript does it support?

  • Nabil2387

    salut , j’ai éssaye de l’installer , en fin j’ai installer la llvm et clang mais je suis pas arrivé a installé emscripten , j’ai un erreur comme quoi , Node.Js ne connait pas Clang , commet peut-on pour configuer node pour qu’il puisse connaitre clang ?

  • Do you speak English? Google Translate gives me an idea of what your problem is, but I’m not totally sure. Can you open an Issue on GitHub? https://github.com/jterrace/js.js/issues/new

  • Awesome development, and very interesting project. I think you like javascript.