Contents
Overview¶
This is a short tutorial on the RLBox API. If you are looking for a reference of all APIs, see [Doxygen].
RLBox is a toolkit for sandboxing third-party libraries. The toolkit consists of (1) a Wasm-based sandbox and (2) an API for retrofitting existing application code to interface with a sandboxed library. The Wasm-based sandbox is documented in its corresponding repository. This documentation focuses on the API and the interface you will use when sandboxing code, independent of the underlying sandboxing mechanism.
Why do we need a sandboxing API? Sandboxing libraries without the RLBox API is both tedious and error-prone. This is especially the case when retrofitting an existing codebase like Firefox where libraries are trusted and thus the application-library boundary is blurry. To sandbox a library – and thus to move to a world where the library is no longer trusted – we need to modify this application-library boundary. For example, we need to add security checks in Firefox to ensure that any value from the sandboxed library is properly validated before it is used. Otherwise, the library (when compromised) may be able to abuse Firefox code to hijack its control flow (see [RLBoxPaper] for details). The RLBox API is explicitly designed to make retrofitting of existing application code simpler and less error-prone.
Sandboxing architecture overview As shown in Fig. 1, RLBox ensures that a sandboxed library is memory isolated from the rest of the application – the library cannot directly access memory outside its designated region – and that all boundary crossings are explicit. This ensures that the library cannot, for example, corrupt Firefox’s address space. It also ensures that Firefox cannot inadvertently expose sensitive data to the library (e.g., pointers that would leak its ASLR).

Fig. 1 Sandboxed libraries are isolated from the application and all communication between the sandboxed library and application code is mediated. This ensures that the application code is robust and does not use untrusted, tainted values without checking them.¶
Memory isolation is enforced by the underlying sandboxing mechanism (from the start, when you create the sandbox with create_sandbox()). Explicit boundary crossings are enforced by RLBox (either at compile- or and run-time). For example, with RLBox you can’t call library functions directly; instead, you must use the invoke_sandbox_function() method. Similarly, the library cannot call arbitrary Firefox functions; instead, it can only call functions that you expose with the register_callback() method. (To simplify the sandboxing task, though, RLBox does expose a standard library as described in Standard library.)
When calling a library function, RLBox copies simple values into the sandbox
memory before calling the function. For larger data types, such as structs and
arrays, you can’t simply pass a pointer to the object. This would leak ASLR
and, more importantly, would not work: sandboxed code cannot access application
memory. So, you must explicitly allocate memory in the sandbox via
malloc_in_sandbox() and copy application data to
this region of memory (e.g., via strncpy
).
RLBox similarly copies simple return values and callback arguments. Larger data structures, however, must (again) be passed by sandbox-reference, i.e., via a reference/pointer to sandbox memory.
To ensure that application code doesn’t use values that originate in the sandbox – and may thus be under the control of an attacker – unsafely, RLBox considers all such values as untrusted and taints them. Tainted values are essentially opaque values (though RLBox does provide some basic operators on tainted values). To use a tainted value, you must unwrap it by copying the value into application memory – and thus out of the reach of the attacker – and verifying it. Indeed, RLBox forces application code to perform the copy and verification in sync using verifiction functions.
Example library sandboxing¶
To get a feel for what it’s like to use RLBox, we’re going to sandbox a tiny
library mylib
that has four functions:
// mylib.c:
void hello() {
printf("Hello world from mylib\n");
}
unsigned add(unsigned a, unsigned b) {
return a + b;
}
void echo(const char* str) {
printf("> mylib: %s\n", str);
}
void call_cb(void (*cb) (const char* str)) {
cb("hi again!");
}
This is not the most interesting library, security-wise, but it is complicated enough to demonstrate various RLBox features.
To get started, in our main application file let’s first import the RLBox library:
// main.cpp:
#define RLBOX_SINGLE_THREADED_INVOCATIONS
#define RLBOX_USE_STATIC_CALLS() rlbox_noop_sandbox_lookup_symbol
#include <stdio.h>
#include "mylib.h"
#include "rlbox.hpp"
#include "rlbox_noop_sandbox.hpp"
using namespace rlbox;
...
In our main function, let’s now create a new sandbox (for this example we’re
going to use the NULL sandbox) and call the hello
function:
...
int main(int argc, char const *argv[]) {
// Create a new sandbox
rlbox::rlbox_sandbox<rlbox_noop_sandbox> sandbox;
sandbox.create_sandbox();
// call the library hello function
sandbox.invoke_sandbox_function(hello);
...
Note that we do not call hello()
directly. Instead, we use the
invoke_sandbox_function() method. We can similarly call the
add
function:
...
// call the add function and check the result:
auto ok = sandbox.invoke_sandbox_function(add, 3, 4).copy_and_verify([](unsigned ret){
printf("Adding... 3+4 = %d\n", ret);
return ret == 7;
});
printf("OK? = %d\n", ok);
...
This invocation is a bit more interesting. First, we call add
with
arguments. Second, RLBox ensures that the unsigned
return value that
add
returns is tainted and thus cannot be used without
verification. Here, we call the copy_and_verify
function which copies the value into application memory and runs our verifier
function:
[](unsigned ret){
printf("Adding... 3+4 = %d\n", ret);
return ret == 7;
}
This lambda simply prints the tainted value and returns true
if it is
7
. A compromised library could return any value and if we use this value
to, say, index an array this could potentially introduce an out-of-bounds
memory access.
Let’s now call the echo
function which takes a slightly more interesting
argument: a string. Here, we can’t simply pass a string literal as an argument:
the sandbox cannot access application memory where this would be allocated.
Instead, we must allocate a buffer in sandbox memory and copy the string we
want to pass to echo
into this region:
...
const char* helloStr = "hi hi!";
size_t helloSize = strlen(helloStr) + 1;
// allocate memory in the sandbox:
auto taintedStr = sandbox.malloc_in_sandbox<char>(helloSize);
// copy helloStr into the sandbox:
std::strncpy(taintedStr.unverified_safe_pointer_because(helloSize, "writing to region"), helloStr, helloSize);
...
Note that taintedStr
is actually a tainted string: it
lives in the sandbox memory and could be written to by the (compromised)
library code concurrently. As such, it’s unsafe for us to use this pointer
without verification. Above, we use the unverified_safe_pointer_because verifier which basically removes the taint without
any verification. This is safe because we copy the helloStr
to sandbox
memory: at worst, the sandboxed library can overwrite the memory region pointed
to by taintedStr
and crash when it tries to print it.
Now, we can just call the function and free the allocated string:
...
sandbox.invoke_sandbox_function(echo, taintedStr);
sandbox.free_in_sandbox(taintedStr);
...
Finally, let’s call the call_cb
function. To do this, let’s first define a
callback for the function to call. We define this function above the main
function:
...
void hello_cb(rlbox_sandbox<rlbox_noop_sandbox>& _,
tainted<const char*, rlbox_noop_sandbox> str) {
auto checked_string =
str.copy_and_verify_string([](std::unique_ptr<char[]> val) {
return std::strlen(val.get()) < 1024 ? std::move(val) : nullptr;
});
printf("hello_cb: %s\n", checked_string.get());
}
...
This callback is called with a string. We thus call the string verification function with a simple verifier:
...
[](std::unique_ptr<char[]> val) {
return std::strlen(val.get()) < 1024 ? std::move(val) : nullptr;
}
...
This verifier moves the string if it’s length is less than 1KB and otherwise
returns the nullptr
. In the callback we simply print this (potentially
null) string.
Let’s now continue in main
, register the callback – otherwise RLBox will
disallow the library-application call – and pass the callback to the
call_cb
function:
...
// register callback and call it
auto cb = sandbox.register_callback(hello_cb);
sandbox.invoke_sandbox_function(call_cb, cb);
...
Finally, let’s destroy the sandbox and exit:
...
// destroy sandbox
sandbox.destroy_sandbox();
return 0;
}
Core API¶
In this section we describe a large part of the RLBox API you are likely to encounter when porting libraries. The API has some more advanced features and types that are necessary but not as commonly used (see [Doxygen]). In most cases the RLBox type system will give you an informative error if and how to use these features.
Creating (and destroying) sandboxes¶
RLBox encapsulates sandboxes with rlbox_sandbox class. For now, RLBox supports two sandboxes: a Wasm-based sandboxed and the null sandbox. The null sandbox doesn’t actually enforce any isolation, but is very useful for migrating an existing codebase to use the RLBox API. In fact, in most cases you want to port the existing code to use RLBox when interfacing with a particular library and only then switch over to the Wasm-based sandbox.
-
template<typename
T_Sbx
>
classrlbox_sandbox
: protected T_Sbx¶ Encapsulation for sandboxes.
- Template Parameters
T_Sbx
: Type of sandbox. For the null sandbox this isrlbox_noop_sandbox
-
class
rlbox_noop_sandbox
¶ Class that implements the null sandbox. This sandbox doesn’t actually provide any isolation and only serves as a stepping stone towards migrating an application to use the RLBox API.
-
template<typename ...
T_Args
>
autorlbox::rlbox_sandbox
::
create_sandbox
(T_Args... args)¶ Create a new sandbox.
- Template Parameters
T_args
: Arguments passed to the underlying sandbox implementation. For the null sandbox, no arguments are necessary.
Creating sandboxes is mostly straightforward. For the null sandbox, however,
you need to add a #define
at the top of your entry file, before you include
the RLBox headers:
#define RLBOX_USE_STATIC_CALLS() rlbox_noop_sandbox_lookup_symbol
...
rlbox::rlbox_sandbox<rlbox_noop_sandbox> sandbox;
sandbox.create_sandbox();
-
auto
rlbox::rlbox_sandbox
::
destroy_sandbox
()¶ Destroy sandbox and reclaim any memory.
It’s important to destroy a sandbox after you are done with it. This ensures that the memory footprint of sandboxing remains low. Once you destroy a sandbox though, it is an error to use the sandbox object.
Calling sandboxed library functions¶
RLBox disallows code from calling sandboxed library functions directly. Instead, application code must use the invoke_sandbox_function() method.
-
invoke_sandbox_function
(func_name, ...)¶ Call sandbox function.
- Return
Tainted value or void.
- Parameters
func_name
: The sandboxed library function to call....
: Arguments to function should be simple or tainted values.
Though this function is defined via macros, RLBox uses some template and macro magic to make this look like a sandbox method. So, in general, you can call sandboxed library functions as:
// call foo(4)
auto result = sandbox.invoke_sandbox_function(foo, 4);
Exposing functions to sandboxed code¶
Application code can expose callback functions to sandbox via register_callback(). These functions can be called by the sandboxed code until they are unregistered.
-
template<typename
T_RL
, typenameT_Ret
, typename ...T_Args
>
sandbox_callback<T_Cb_no_wrap<T_Ret, T_Args...> *, T_Sbx>rlbox::rlbox_sandbox
::
register_callback
(T_Ret (*func_ptr)(T_RL, T_Args...)) Expose a callback function to the sandboxed code.
- Return
Wrapped callback function pointer that can be passed to the sandbox.
- Parameters
func_ptr
: The callback to expose.
- Template Parameters
T_RL
: Sandbox reference type (first argument).T_Ret
: Return type of callback. Must be tainted or void.T_Args
: Types of remaining callback arguments. Must be tainted.
The type signatures of register_callback() function is a bit daunting. In short, the function takes a callback function and returns a function pointer that can be passed to the sandbox (e.g., via invoke_sandbox_function()).
A callback function is a function that has a special type:
The first argument of the function must be a reference a sandbox object.
The remaining arguments must be tainted.
The return value must be tainted or
void
. This ensures that the application cannot accidentally leak data to the sandbox.
Forcing arguments to be tainted forces the application to handled values coming from the sandbox with care. Dually, the return type ensures that the application cannot accidentally leak data to the sandbox.
-
template<typename
T_Ret
, typename ...T_Args
>
voidrlbox::rlbox_sandbox
::
unregister_callback
(void *key)¶ Unregister a callback function and disallow the sandbox from calling this function henceforth.
Tainted values¶
Values that originate in the sandbox are tainted. We use a special tainted type tainted to encapsulate such values and prevent the application from using tainted values unsafely.
-
template<typename
T
, typenameT_Sbx
>
classtainted
¶
RLBox has several kinds of tainted values, beyond tainted. Thse, however, are slightly less pervasive in the surface API.
-
template<typename
T
, typenameT_Sbx
>
classtainted_volatile
: public rlbox::tainted_base_impl<tainted_volatile, T, T_Sbx>¶ Tainted volatile values are like tainted values but still point to sandbox memory. Dereferencing a tainted pointer produces a tainted_volatile.
-
class
tainted_boolean_hint
¶ Tainted boolean value that serves as a “hint” and not a definite answer. Comparisons with a tainted_volatile return such hints. They are not
tainted<bool>
values because a compromised sandbox can modify tainted_volatile data at any time.
Unwrapping tainted values¶
To use tainted values, the application can copy the value to application memory, verify the value, and unwrap it. RLBox provides several functions to do this.
-
template<typename
T_Func
>
autorlbox::tainted_base_impl
::
copy_and_verify
(T_Func verifier) const¶ Copy tainted value from sandbox and verify it.
- Return
Whatever the verifier function returns.
- Parameters
verifer
: Function used to verify the copied value.
- Template Parameters
T_Func
: the type of the verifier.
For a given tainted type, the verifier should have the following signature:
Tainted type kind |
Example type |
Example verifier |
---|---|---|
Simple type |
|
|
Pointer to simple type |
|
|
Pointer to class type |
|
|
Pointer to array |
|
|
Class type |
|
|
In general, the return type of the verifier T_Ret
is not constrained and can
be anything the caller chooses.
-
template<typename
T_Func
>
autorlbox::tainted_base_impl
::
copy_and_verify_range
(T_Func verifier, std::size_t count) const¶ Copy a range of tainted values from sandbox and verify them.
- Return
Whatever the verifier function returns.
- Parameters
verifer
: Function used to verify the copied value.count
: Number of elements to copy.
- Template Parameters
T_Func
: the type of the verifier. If the tainted type isint*
thenT_Func = T_Ret(*)(unique_ptr<int[]>)
.
-
template<typename
T_Func
>
autorlbox::tainted_base_impl
::
copy_and_verify_string
(T_Func verifier) const¶ Copy a tainted string from sandbox and verify it.
- Return
Whatever the verifier function returns.
- Parameters
verifer
: Function used to verify the copied value.
- Template Parameters
T_Func
: the type of the verifierT_Ret(*)(unique_ptr<char[]>)
-
template<typename
T_Func
>
autorlbox::tainted_base_impl
::
copy_and_verify_address
(T_Func verifier)¶ Copy a tainted pointer from sandbox and verify the address.
This function is useful if you need to verify physical bits representing the address of a pointer. Other APIs such as copy_and_verify performs a deep copy and changes the address bits.
- Return
Whatever the verifier function returns.
- Parameters
verifier
: Function used to verify the copied value.
- Template Parameters
T_Func
: the type of the verifierT_Ret(*)(uintptr_t)
In some cases it’s useful to unwrap tainted values without verification.
Sometimes this is safe to do and RLBox provides a method for doing so
called unverified_safe_because
Since pointers are special (sandbox code may modify the data the pointer points to), we have a similar function for pointers called unverified_safe_pointer_because. This API requires specifying the number of elements being pointed to for safety.
We however provide additional functions that are especially useful during migration:
-
template<template<typename, typename> typename
T_Wrap
, typenameT
, typenameT_Sbx
>
classtainted_base_impl
¶ Public Functions
-
auto
UNSAFE_unverified
()¶ Unwrap a tainted value without verification. This is an unsafe operation and should be used with care.
-
auto
UNSAFE_sandboxed
(rlbox_sandbox<T_Sbx> &sandbox)¶ Like UNSAFE_unverified, but get the underlying sandbox representation.
For the Wasm-based sandbox, this function additionally validates the unwrapped value against the machine model of the sandbox (LP32).
- Parameters
sandbox
: Reference to sandbox.
-
auto
These functions are also available for callback
Danger
Unchecked unwrapped tainted values can be abused by a compromised or malicious library to potentially compromise the application.
Operating on tainted values¶
Unwrapping tainted values requires care – getting a verifier wrong could lead
to a security vulnerability. It’s also not cheap: we need to copy data to the
application memory to ensure that the sandboxed code cannot modify the data
we’re tyring to verify. Lucikly, it’s not always necessary to copy and verify:
sometimes we can compute on tainted values directly. To this end, RLBox defines
different kinds of operators on tainted values, which produce tainted values.
This allows you to perform some computations on tainted values, pass the values
back into the sandbox, and only later unwrap a tainted value when you need to.
operators like +
and -
on tainted values.
Class of operator |
Supported operators |
---|---|
Arithmetic operators |
|
Relational operators |
|
Logical operators |
|
Bitwise operators |
|
Compound operators |
|
Pointer operators |
|
When applying a binary operator like <<
to a tainted value and an untainted
values the result is always tainted.
RLBox also defines several comparison operators on tainted values that sometime unwrap the result:
Operators
==
,!=
on tainted pointers is allowed if the rhs isnullptr_t
and return unwrappedbool
.Operator
!
on tainted pointers retruns an unwrappedbool
.Operators
==
,!=
,!
on non-pointer tainted values return atainted<bool>
Operators
==
,!=
,!
on tainted_volatile values returns a tainted_boolean_hintOperators
&&
and||
on booleans are only permitted when arguments are variables (not expressions). This is because C++ does not permit safe overloading of && and || operations with expression arguments as this affects the short circuiting behaviour of these operations.
Standard library¶
RLBox provides several helper functions to application for handling sandboxed memory regions and values.
-
template<typename
T_Sbx
, typenameT_Rhs
, typenameT_Val
, typenameT_Num
, template<typename, typename> typenameT_Wrap
>
T_Wrap<T_Rhs *, T_Sbx>rlbox
::
memset
(rlbox_sandbox<T_Sbx> &sandbox, T_Wrap<T_Rhs *, T_Sbx> ptr, T_Val value, T_Num num)¶ Fill sandbox memory with a constant byte.
-
template<typename
T_Sbx
, typenameT_Rhs
, typenameT_Lhs
, typenameT_Num
, template<typename, typename> typenameT_Wrap
>
T_Wrap<T_Rhs *, T_Sbx>rlbox
::
memcpy
(rlbox_sandbox<T_Sbx> &sandbox, T_Wrap<T_Rhs *, T_Sbx> dest, T_Lhs src, T_Num num)¶ Copy to sandbox memory area.
-
template<typename
T_Lhs
, typenameT_Rhs
, typenameT_Sbx
, template<typename, typename> typenameT_Wrap
>
tainted<T_Lhs, T_Sbx>rlbox
::
sandbox_reinterpret_cast
(const T_Wrap<T_Rhs, T_Sbx> &rhs)¶ The equivalent of a reinterpret_cast but operates on sandboxed values.