Wikipedia defines a software sandbox as follows[1]:
In computer security, a sandbox is
a security mechanism for separating running programs. 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. A sandbox typically provides a tightly
controlled set of resources for guest programs to run in, such as scratch space
on disk and memory. Network access, the ability to inspect the host system or
read from input devices are usually disallowed or heavily restricted.
In the sense of providing a highly
controlled environment, sandboxes may be seen as a specific example of
virtualization. Sandboxing is frequently used to test unverified programs that
may contain a virus or other malicious code, without allowing the software to
harm the host device.
In the physical world, in which children play with sand,
there are two common styles of sandbox. The first is constructed from four
equally sized wooden planks, each stood on its long edge to form a square box, fastened
in the corners, and then filled with sand. The second style is typified by a
large green plastic turtle, whose “turtle shell” is the removable top that
keeps the rain out, and whose “body” is the hollow bowl that keeps the sand in.
Both styles hold sand and allow a child to dig tunnels and build sand-castles,
but there is one major difference: When a child tunnels too deeply in the wooden-sided
sandbox, the tunnel burrows past the sand and into the soil beneath, while the tunnel
depth in the turtle sandbox is strictly limited by the plastic bowl.
Software sandboxes tend to mirror these physical types, in
that the dirt often lies beneath. In
other words, the sandbox attempts to protect the resources of the system, but a
determined programmer will eventually be able to find a way through. The only
way that a language runtime as a sandbox can ensure the protection of the
underlying resources of a system is for the sandbox itself to completely lack
the ability to access those resources. Thus, the purpose of the sandbox is to
defend against privilege escalation:
Privilege escalation is the act of
exploiting a bug, design flaw or configuration oversight in an operating system
or software application to gain elevated access to resources that are normally
protected from an application or user. The result is that an application with
more privileges than intended by the application developer or system
administrator can perform unauthorized actions[2].
As a language runtime designer, it is not sufficient to
simply distrust the application code itself; one must distrust the entire graph
of code that is reachable by the application code, including all third party
libraries, including the language's own published runtime libraries, and
including any internal libraries that come with the runtime that are accessible.
Or, put another way, if there is a possible attack vector that is reachable, it
will eventually be exploited. To truly seal the bottom of the sandbox, it is
necessary to disallow resource access through
the sandbox altogether, and to enforce that limit via transitive closure.
But what good is a language that lacks the ability to work
with disks, file systems, networks, and network services? Such a language would
be fairly worthless. Ecstasy addresses this requirement by employing dependency injection, which is a form of
inversion of control. To comprehend
this, it is important to imagine the container functionality not as a sandbox,
but as a balloon, and our own universe as the primordial example.
Like an inflated balloon, the universe defines both a
boundary and a set of contents. The boundary is defined not so much by a
location, but rather by its impermeability – much like the bottom of the green
plastic turtle sandbox. In other words, the content of the universe is fixed[3],
and nothing from within can escape, and nothing from without can enter. From
the point of view of someone within our actual universe, such as you the
reader, there is no boundary to the universe, and the universe is seemingly
infinite.
However, from outside of the universe, the balloon barrier
is quite observable, as is the creation and destruction of the balloon.
Religiously speaking, one plays the part of God when inflating a balloon, with
complete control over what goes through that one small – and controllable –
opening of the balloon.
It is this opening through which dependency injection of
resources can occur. When an application needs access to a file system, for
example, it supplicates the future creator of its universe by enumerating its
requirement as part of its application definition. These requirements are
collected by the compiler and stored in the resulting application binary; any
attempt to create a container for the application will require a file system resource
to be provided.
And there are two ways in which such a resource can be
obtained. First of all, the resource is defined by its interface, so any
implementation of that interface, such as a mock
file system or a fully emulated – yet completely fake! – file system would do.
The second way that the resource can be obtained is for the code that is
creating the container to have asked for it in the same manner – to declare a
dependency on that resource, and in doing so, force its own unknown creator to
provide the filing system as an answer to prayer.
As the saying goes, it’s turtles all the way down. In this
case, the outermost container to be created is the root of the container
hierarchy, which means that if it requires a filing system, then the language
runtime must inject something that provides the interface of a filing system,
and that resource that is injected might even be a representation of the actual
filing system available to the operating system process that is hosting the
language runtime.
And here we have a seemingly obvious contradiction: What is
the difference between a language that attempts to protect resources by hiding
them at the bottom of a sandbox container, versus a language that provides
access to those same resources by injecting them into a container? There are
several differences, but let’s start with an obvious truth: Perfection in the
design of security is difficult to achieve, and even harder to prove the
correctness of, so it is important to understand that this design does not
itself guarantee security. Rather, this design seeks to guarantee that only one
opening in the balloon – and anything that is injected through that opening –
needs to be protected, and the reason is self-evident: Transitive closure. By
having nothing naturally occurring in the language runtime that represents an
external resource, there is simply no surface area within the language runtime
– other than the injected dependencies themselves – that is attackable.
Second, the separation of interface and implementation in
the XVM means that the implementation of the resource is not visible within the
container into which it is injected. While this pre-introduces a number of
language and runtime concepts, the container implementation only makes visible
the surface area of the resource injection interface
– not of the implementation! This holds true even with introspection, and furthermore
the injected resources are required to be either fully immutable, or completely
independent services.
Third, this design precludes the possibility of native code
within an Ecstasy application; native functionality can only exist outside of
the outermost container and thus outside of the language runtime itself, and
can only be exposed within the language runtime via a resource injected into a
container, subject to all of the constraints already discussed.
Lastly, as has been described already, the functionality
that is injected is completely within the control of the injector, allowing the
requested functionality to be constrained in any arbitrary manner that the
injector deems appropriate.
While it is possible to introduce security bugs via
injection, the purpose of this design is to minimize the scope of potential
security bugs to the design of the relatively small number of interfaces that
will be supported for resource injection, and to the various injectable
implementations of those interfaces.
This, in combination with your ideas on modules, really made me think. I will steal it in modified form for my Tailspin language and require every top-level process to explicitly inject its chosen or overridden versions of each module, transitively all the way down.
ReplyDeleteGlad to hear that the ideas are useful!
ReplyDelete