CheerpX: Using WebAssembly to run any programming language in the browser

Alessandro Pignotti
leaningtech
Published in
9 min readNov 16, 2021

TL;DR — We built a WebAssembly based virtual machine to run X86 binaries in the browser. It’s called CheerpX. You can run any REPL environment with it. Example: https://repl.leaningtech.com/?python3

Historically, JavaScript has been the programming language of the Web. This changed with the introduction of WebAssembly, albeit in a fairly indirect way. WebAssembly itself is not intended to be written manually. You certainly can do that, but similarly to traditional assembly languages, it’s really not advised.

WebAssembly is, fundamentally, a compiler target: a binary representation emitted by a specialized program from a different source language.

This is also reflected in the design of WebAssembly engines themselves, which assume that the incoming bytecode is already well optimized, and most of their internal optimization pipeline is disabled. This is in stark contrast to JavaScript compilation, where decades of work from the best minds in the field have been invested to make low quality JavaScript run fast.

At the time of this writing, WebAssembly is most commonly generated from C/C++ code using LLVM-based toolchains. When effort has been made to port modern dynamic programming languages, such as Python, the preferred solution has been to compile a C/C++ implementation. To achieve this, stripped down versions may be used, for example disabling JIT support or modules that depend on natively built components.

Porting a significant C/C++ codebase to WebAssembly is not necessarily a simple endeavor. A programming language implementation will most likely have multiple target-dependent code paths or definition, so the effort required is not dissimilar from porting the language to a new native architecture.

Then, of course, if after porting Python, you want to port Ruby… well, you need to start from scratch.

A better solution

It is already possible to find browser based REPLs for some of the most popular programming languages. Some of them are, more or less, complete ports of the corresponding engines from C/C++. Some other services “cheat”, by running the program somewhere on a cloud VM and only using the browser to interact with the user. Cloud based solutions are trivial to implement but also have significant drawbacks in terms of computational costs and user data ownership.

At Leaning Technologies, we like to build products that solve difficult, general problems and can support multiple use cases at the same time. An example is our flagship technology CheerpX, a WebAssembly powered Virtual Machine whose goal is to safely and efficiently run unmodified X86 binary code in the browser.

At its core, CheerpX is an emulator for the X86 architecture. Obviously a simple emulator will not get you very far in terms of performance, so we implemented a sophisticated JIT compiler that is able to generate efficient WebAssembly representations for hot code. The whole process is 100% automated and requires no metadata or assumptions whatsoever. The system is so robust that it can actually deal with applications that generate new code internally at runtime, and even with self-modifying code. This means that you can actually run things like NodeJS, including the full V8 engine with its remarkable JIT and code garbage collection.

As you can imagine, this is a fiendishly complicated problem. When designing CheerpX we decided to only focus on user-mode code (Ring 3), ignoring (for now) the complexities of system level (Ring 0) features. To close the gap and let real world applications run, we have implemented (a subset) of the Linux system calls interface.

By now it is probably clear that CheerpX can be used to run any programming language REPL in the browser. Instead of porting each language environment manually to WebAssembly, we simply let the whole Linux/X86 build run on top of the CheerpX VM.

To reach a satisfactory performance and UX, a couple of additional problems had to be solved:

Terminal output: CheerpX provides partial support for terminal interaction in the host web page. REPLs tend to use advanced terminal manipulation to enhance the user experience, most usually by including libreadline. Instead of investing effort in improving our terminal implementation we have decided, for once, to use an external library: XTerm.js. It works great in most cases, although we had encountered some issues with a couple of the demos.

Data storage: Our first iteration of these demos stored data by unzipping an archive into an IndexedDB powered file-system, with each entry in the database being a file or directory.

This solution did not scale well. Isolating the minimal archive to run a given REPL is, in general, an error-prone process. By shipping too few files we could cause runtime failures, while too many files would increase the download time needlessly. Actually, both situations could even happen at the same time! Moreover REPLs seem to like prodding the filesystem all over the place, with the worst offender being probably NodeJS.

We have seen even mildly complex scenarios attempting to access thousands of files. IndexedDB did not seem to be very happy about such access patterns.

The solution to this problem was to integrate a custom Ext2 implementation in CheerpX. Disks are represented by HTTP assets, which can be downloaded block-by-block and are cached by a CDN. Since only the required blocks are downloaded we can avoid any effort in optimizing the image size.

We were actually able to stop maintaining different images for each REPL. We can literally debootstrap a Debian setup inside an Ext2 image, install the packages we want and copy it over to an HTTP blob storage. Downloaded blocks are internally cached in IndexedDB to both reduce download time on page reloads while also allowing privacy preserving disk write support. Any data you edit with this configuration is private to your own browser. We don’t see anything.

Let’s see what we can do by putting all this machinery together, shall we?

An overview of the architecture used for these demos.

Python3

REPL: https://repl.leaningtech.com/?python3

Python is an interpreted, high-level, general-purpose programming language. It’s dynamically typed and garbage collected. It supports both procedural, object-oriented and functional programming.

This demo features the standard CPython implementation, which does not include a JIT. It compiles the source files (.py) into intermediate bytecode files (.pyc). This is an interesting point: it means that whatever implementation of the underlying filesystem we use, it has to support persistent writes.

Somewhat surprisingly, we found a possible POSIX non-compliance in the way pyc files are written. In our first implementation of the write syscall CheerpX would only save up to a disk block, expecting the application to try again if data was only partially written.

We believe this to be allowed by POSIX, but real world code seems to have a different opinion. We have since then improved CheerpX to allow “unbounded” writes and reads. Generally speaking nothing can truly be unbounded, neither in CheerpX or on native, so we wonder if Python3 only works because the cache files are relatively small in practice.

But don’t worry, this bug is most likely not exploitable, and worst case it should be a denial of service. Nothing critical runs on Python right?

NodeJS

REPL: https://repl.leaningtech.com/?nodejs

NodeJS is a server-side JavaScript environment built on top of the state-of-the-art JavaScript engine, V8. A REPL is included, as usual, for convenience.

In the past, whenever we mentioned running NodeJS/V8 in the browser, we have seen people expecting a stripped down pure-interpreter version. That is not what you see here. This is the unmodified executable shipped with Debian, where the JIT is enabled and used.

Curiously, the Debian build is not as optimized as it should be. It ships a very thin nodejs executable whose only purpose is to load libnode.so containing the whole program. The shared library does not make proper use of internalized symbols, which means that most (all?) of the function calls happen via the PLT. After noticing this, we have improved the existing support for inlining and devirtualization in CheerpX to effectively convert these indirect calls to direct calls in most cases.

Ruby

REPL: https://repl.leaningtech.com/?ruby

A run of-the-mill interpreter. Nothing very interesting was found while working on this. One of my side projects is to actually run the full Rails environment as well, including opening rendered pages inside a separate iframe. This can be done and I have a working prototype already. If there is some interest we may get to publish it as well.

Cling

REPL: https://repl.leaningtech.com/?cling

C++ is not traditionally interpreted, but like any other programming language it is possible to do so. We used cling (https://root.cern/cling/), an interactive C++ interpreter built on top of LLVM/Clang and developed by CERN. It has a command-line prompt and uses a JIT compiler to evaluate the input line-by-line. Since it’s built on top of Clang, it can parse everything that Clang can parse.

It’s an extremely complex piece of software that adds to the normal C++ instructions a series of interpreter-specific C++ extensions aimed at making the use of Cling’s shell easier. It features an internal set of commands to print the current AST and to load and execute files. It additionally manipulates the user code into valid C++ code so that it’s possible to compile it with the underlying Clang.

Example:

cling$ int i = 1; fun(i++) // perfectly valid Cling instruction

Becomes

int i = 1; // global declaration

void wrap_0() { fun(i++); }

Of all these demos, Cling is the only one that does not ship by default with Debian. It was installed separately from archives provided by the developers. The cling executable itself is 100+ MBs. To get this monster to load in a reasonable amount of time we implemented lazy loading of ELF executables in CheerpX.

Most likely the excessive size is caused by the executable not being properly stripped of debug info. We could have stripped it ourselves, but since the point of this exercise is proving that CheerpX works on binaries without any intervention, we decided against it. Just be aware that this demo is not as speedy as others.

Lua / Luajit

(Lua) REPL: https://repl.leaningtech.com/?lua

(Luajit) REPL: https://repl.leaningtech.com/?luajit

Lua ships with a very simple interpreter that worked on CheerpX out of the box. Since we like to do complex stuff, we decided to add Luajit to the mix too. We understand it to be a completely separate project from the official Lua interpreter.

The main issue we found for these demos was user input handling. All the other REPLs use raw terminal input: they are happy to receive control characters such as backspace and newline and process them internally. Lua/Luajit expects cooked terminal input. Xterm.js is not designed for this use case, and has no line-editing support. We have worked the problem around by supporting keystroke echo on our side, and remapping the ENTER key to the expected character. Please note that full line-editing support in CheerpX is lacking, although it can be certainly implemented if need be.

What’s next?

Of course, we did not go through all the complexity of implementing CheerpX just for these demos.

CheerpX is the foundational technology to CheerpX for Flash, a Flash emulator to run legacy Adobe Flash content in pure HTML5. CheerpX for Flash works by running the Flash player (an X86 binary for Linux) in a way not dissimilar to what we have shown in this post.

We believe that the REPLs themselves may be useful in the education / training sector, or to let users fiddle with any library (for any programming language) directly from the documentation page. If this sound useful to you, or you have even better ideas, get in touch: info@leaningtech.com

But, really, this is intended to be just the first step of a much more ambitious demo. As we said, we have a full Debian disk image, with dozens of packages installed, and a solid solution to run a single app from it. We want to get to a point where we can drop the users into bash, and let them do what they want.

We are talking about an always available, zero-cost, virtual machine with guaranteed data privacy.

Does that sound not interesting enough? Well, what if you could spawn XOrg and graphical applications on top? What about a full desktop environment?

I hope this is as exciting for you, as it is for us. And if you think you have the perfect use case for our tech, let us know.

https://twitter.com/alexpignotti

https://twitter.com/leaningtech

https://leaningtech.com/

info@leaningtech.com

--

--