The Age of Sandboxes
(I)
Twenty-three years ago — on December 4, 1995 — the team led by Brendan Eich added to Netscape Navigator a clumsy chimera of a language, an awkward hybrid of Scheme’s ideas, Self’s prototype-based object model, and Java’s syntax: JavaScript. No one could have foreseen that the age of software sandboxes had quietly begun.
Before browsers existed, installing or running a normal piece of software (viruses, worms, and malware aside) required the authorization of the computer’s owner. Authorization is a tiresome affair — to use you, I must trust you: trust that you won’t touch files, data, or even programs you have no business with; trust that you’ll get along with the other programs on my system. And most computer owners lack the ability to make that judgment. JavaScript in the browser broke this convention: the moment I visit your web page, the code on that page begins executing without authorization, consuming my computer’s resources. This creates a serious security hazard — how do you keep unauthorized code from touching my local data? So JavaScript’s capabilities were confined within the sandbox the browser provides: it cannot access data outside the storage the browser grants it, its network access is limited, its use of input/output devices is limited; in short, under the browser’s rules it may not cross the boundary by so much as a step.
If we cast our gaze back another twenty or thirty years, to the age of the mainframe (IBM S/360), we find that people were already using virtualization technology to provide each visitor with an isolated sandbox of full hardware virtualization, in order to use precious computing resources more properly and more safely. Though the means and the scenarios are very different from the browser’s sandbox — the former serving the server side, the latter the client side — the goals are broadly the same: isolation, security, concealment.
Wikipedia defines a sandbox thus:
In computer security, a “sandbox” is a security mechanism for separating running programs, usually in an effort to mitigate system failures or software vulnerabilities from spreading. It is often used to execute untested or untrusted programs or code, possibly from unverified or untrusted third parties, suppliers, users or websites, without risking harm to the host machine or operating system.
In fact, sandboxes in the broader sense are everywhere in computer systems. The OS isolates bare metal from application software, letting the application believe it has the whole system to itself; virtual memory technology isolates physical memory from virtual memory, making it hard for application software to meddle with anyone else’s memory; the VMM/container isolates the host OS from the guest OS/application, letting them run in a safe, exclusive environment.
(II)
The internet feast of the past ten years pushed sandbox technology to a new peak. On the client side, more and more application logic runs in the browser, and so more and more scenarios get pulled into the browser’s sandbox — first CORS overcame JavaScript’s same-origin constraint on network access, then, step by step, WebRTC became a standard. On the server side, server virtualization gave birth to cloud computing, and the microservices transformation led by Docker is now driving the serverless revolution.
Consider the JavaScript sandbox in the browser and the serverless sandbox: they share many of the same characteristics:
- The sandbox provides the necessary interfaces for dealing with the outside world while erecting an impassable firewall, preventing code inside the sandbox from reaching anything beyond its permissions.
- Sandboxes exist to serve untrusted code: in the browser, we don’t trust the web page we’re visiting; on the server, we don’t trust the tenant’s application.
In my view, compared with the browser’s sandbox, every current serverless sandbox implementation is clumsy. The browser carried no historical baggage; when Netscape made its epoch-making introduction of JavaScript, it was blazing its own trail, free to “follow its heart’s desire without overstepping the rules.” Serverless, by contrast, follows the long-established path of isolating the host OS with a guest OS, and merely works on making that guest OS layer lighter and thinner, and cold starts faster.
There is a compelling reason for doing it this way. Application software running on servers, for the most part, needs a POSIX-compatible operating system as a dependency (let’s leave Windows out of the discussion). Creating threads, accessing files, accessing the network — these are all system calls. So for a sandbox there are two choices: either I fully implement a set of POSIX interfaces and provide them to application software as a runtime, or I take an off-the-shelf POSIX interface — some Linux distribution, say — and simply wrap it with container technology. Obviously the latter costs far less.
But the former is the better solution. The contrast between the two is like that between KVM, which takes the full-virtualization path, and QEMU, which takes the emulation path.
With the benefit of hindsight, I think Node.js inadvertently missed its chance to become the (sole) leader of server-side sandbox technology. JavaScript, born inside a sandbox, together with the V8 engine that already had complete sandboxing capability, could have blazed an untrodden trail through the red ocean of server-side development: carefully expanding the sandbox’s boundaries, making Node.js an engine that can run untrusted code on the server (rather than just another backend language). For example:
- Node.js code may only access the working directory and the resources within it; external directories are inaccessible unless explicitly mounted in the sandbox.
- The Node.js sandbox can configure limits on memory, CPU, disk, and other resources, restricting the code’s ability to consume them.
- Node.js code may only access the network ports permitted by the sandbox’s firewall rules and local Unix domain sockets, restricting the code’s network access.
- …
These restrictions pose no problem for most current Node.js code and let it run normally. Yet they could greatly expand the boundaries of what Node.js is used for, even making it a serverless platform in its own right. Unfortunately, having escaped the browser’s fences, Node.js was like a mischievous kid freed from the gaokao, tearing off layer after layer of restraint as it raced headlong into the adult world “enchanted by greed, anger, delight, aversion, and rage” — the club of backend development languages.
We know that in the backend world, the prevailing model is role-based security, an approach inherited from the mainframe era that is becoming the scourge of our time. Anyone who has done DevOps knows that a service deployed in production should be run under a user with the least privilege, so that even if a vulnerability lets malicious code seize the user’s permissions, it won’t be able to do something like cat /etc/passwd. Even so, role-based security is exceedingly clumsy: it cannot stop malicious code from eating up the host machine’s CPU, memory, disk, TCP ports, and so on. That’s why we need the finer-grained, capability-based security listed above.
Fortunately, another language now strongly backed by mainstream browsers — WebAssembly — has shown great restraint on its path out of the browser and toward running natively. Could Node.js’s loss become WASI’s (WebAssembly System Interface’s) gain, making it the future of serverless?
(III)
The opening of Romance of the Three Kingdoms declares: “The empire, long divided, must unite; long united, must divide.” So for sandbox technology, is it possible that at some point in the future, the browser’s sandbox and the serverless sandbox will achieve a grand unification? Or, put another way: as an individual user, could I run any untrusted software on my own operating system without worrying about the system being compromised — just as I can now open applications in the browser with complete peace of mind?
Consider how a typical serverless app is triggered today. Take AWS: a static website hosted on S3 is visited by a user; JavaScript on the page fires a JavaScript request; the request reaches API Gateway, which wakes up a serverless container that runs the corresponding code (which may access a table in some database); and so on until the user gets a response.
Now reverse the scenario: JavaScript on the page fires a request that reaches a piece of server-side code running locally. On first use, if the service isn’t installed locally, the browser asks the user whether to install it — and because there’s no worry about the system being compromised, the user chooses to install. After that, the server-side logic runs on the user’s local machine as well, and naturally the data is stored on the user’s side too (the user can decide whether to sync it to their own cloud storage). Likewise, just as with today’s serverless, when the user stops using the service for a while, the service is killed to save resources.
This would give rise to many interesting applications. On a small scale, software like Keybase and Slack could be replaced by something more secure and more privacy-conscious. On a large scale, as people grow weary of the over-exposed social networks, this is also a way to build a decentralized mini-Facebook — and what’s more wonderful, even if this network has a billion users, what you store locally is only the information about the interactions between you and your friends; the space complexity is nearly equivalent to a constant.
Perhaps one day your personal computer will be such a sandbox: hosting untrusted applications and their data — LinkedIn, Slack, Facebook, Yelp. It is itself a “super server.”