Maintaining a Single Runtime
This section describes several technical details related to transformations intended to maintain the original application behavior.
Generally, since BreakApp starts its transformation from the object returned from a module (e.g.,
module.exports), values are associated with a name: the name of the attribute associated with that value. However, not all values in messages include a meaningful name. For instance, a function can be anonymous and an object can just be a bytebuffer. To facilitate cross-compartment addressing, the child compartment maintains a hash table mapping object and function IDs (e.g., SHA256 checksums) to their in-compartment pointers. These IDs can be thought as distributed, shared-memory pointers which RPCs include in their messages. Whenever it receives an RPC message, BreakApp on the child compartment looks into the table and routes freshly-deserialized arguments to the right function or object method.
DAG Structure and Reference Equality
The creation of object copies during transformation and serialization breaks reference equality. BreakApp takes care to preserve it. When an RPC leads to a new memory alias in the remote compartment, the return message from the remote compartment will include an
alias entry containing the remote object ID. BreakApp on the child compartment then creates and returns a reference to an existing object. The same consideration must be extended to preserving reference equality for the root of the DAG between RPCs. A common pattern in many languages is to have methods that return
self; such code would break if the return value of the RPC was a fresh copy of the method receiver.
Messages get assigned a sequence number. Although communication primitives are reliable, messages should be received at the correct call order. For example, an asynchronous call to the printing function will be shown before the next call to the same function.
Calls to Constructors
Constructors, usually prefixed by the
new keyword, slightly change the semantics of a function call: at the very least, new memory may need to be allocated. The RPC stub uses additional logic to detect this case.1 If the function is indeed called with as a constructor, the RPC message has a special type signifying that the target function should also be called with
new. The return value from a constructor is itself an object whose methods are RPC stubs as described earlier: the true object lies within its compartment.
Move vs. Copy Semantics
It is worth clarifying the distinction between values that are remotely referenced and ones that are copied to the parent compartment. When all nodes in the returned DAG are methods, they are transformed to RPC stubs referencing values that live within the remote compartment. State updates targeting such well-encapsulated modules or objects call directly into the remote object. When some nodes in the DAG are primitive values however, they result in deep copies of values. Writes to such values or the RPC stubs themselves2 need to be detected and propagated to the original object.
To achieve this, we wrap the transformed output DAG with an interposition mechanism that provides reflection capabilities and gets invoked upon attribute accesses. A special BreakApp
Proxy wrapper3 detects and records changes to any of the object’s properties. Property values that are themselves objects require nested proxies (Fig. 2). These state updates are compressed into changesets, and propagated lazily by piggybacking on future RPC calls.
The Class Hierarchy
Objects high in the prototype chain are supported natively. Functionality is either implemented internally in the runtime (e.g., serialization and cryptography modules) or wraps OS-level subsystems (e.g., networking and filesystem modules). In most cases, a copy of these objects can be found in the trusted copy of the runtime (see Section 3.2) which BreakApp includes in the new compartment. Examples include modules such as
fs, and globals such as timer functions and top-level objects. There are cases when this is not possible, however. Specific global or pseudo-global4 objects in the child compartment require redirection to the top-level compartment. Examples of such objects include
process to refer to terminal output and process-level data, respectively.
If compartments live in different address spaces, writes to the child compartment’s out and error streams must be transmitted to the top-level process. Upon first import, the system shadows
error with such redirecting proxies. Similarly, it shadows stream input functions with functions that request this functionality from the top-level compartment, which sends the results back to the child.5
The standard runtime garbage collector (GC) cannot “see through” compartment boundaries to collect objects within translation tables. So, in addition to reflecting method calls between compartments using RPCs, BreakApp also propagates garbage collection events by adding a GC hook to every object that is the result of a transformation. When such an object is about to be collected, BreakApp sends a message to the child compartment to remove any references to this object.
Whole modules are more difficult to go out of scope for the GC to kick in and reclaim their memory. This is because there are multiple references to a module in the cache of the loaded modules. However, modules are often unloaded or reloaded manually, which should destroy or restart the child compartment. To maintain this behavior, BreakApp wraps the module cache structure, detects invalidations, and forces the child compartment to exit. Malicious modules cannot cause other modules to exit, because child compartments do not have access to other cache entries.
BreakApp interposes on inter-compartment communication, tracking the load placements and frequency of calls on each channel. It monitors the health (i.e., crashed, not responding) of child compartments periodically and upon remote invocations. It takes curative actions based on the compartment’s status (e.g., restart, kill, or spawn more compartments). This is helpful in cases where the module within the compartment is launching a DoS attach or where asynchronous execution has lead to exceptions. Child compartments use OS primitives (e.g.,
SIGHUP on Linux) to be notified upon parent exit.
Fig. 3 shows the result of a simple module after two stages of transformations. The first transformed the return value (
create) of the module, and the second transformed the return value of a call into the module (a
Point object). These transformations are done during runtime and captured only for illustrative purposes. The left-most column contains most of the original module and its export statement. The right-most column exports a wrapper for
create that serializes arguments and calls back to the original function. The middle two columns show the result of transforming a newly
generateId will store the object to a translation table, and return a remote reference. The transformed
toStr will always call back into the original object, whereas access to its
y fields is
Proxyied through the interposition object.