Untainting different types
As you sandbox more library APIs, you will soon have to start verifying (removing the tainting) objects of different types. Below, we'll go through the APIs used to remove tainting in different scenarios.
In all examples, there are a few things to keep in mind:
-
We will use a comment
<insert security checks here>
to refer that you need to apply domain specific checking to ensure that you only permit values that do not cause memory safety problems in the rest of your program. The tutorial goes through an example of adding security checks in more detail. -
Note that all verifier can return values of any type or no value at all. This gives some flexibility to how you want to manage your data. So for example, you may be untainting an
int
, but can return anunsigned int
or anint*
, orvoid
etc. -
We will refer to the term fundamental types frequently. C/C++ defines the fundamental types as the types built-in to the language, such as
int
,float
etc.
Untainting fundamental types
These types can be untainted with a verifier of the following form
tainted<int> a = ... ; //
int a_verified = a.copy_and_verify([](int val){
// <insert security checks here>
return val;
});
There maybe some scenarios where the application can handle any possible integer
from the sandbox, i.e., the <insert security checks here>
can be left empty.
- An example of this could be when you are calling an sandboxed library function that returns an integer 0 on success and a non-zero error code otherwise. From the host application perspective, you may do something if the returned value is 0, and exit otherwise.
In this scenario, RLBox provides a shorthand API called
unverified_safe_because
which can be used as follows.
tainted<int> a = ... ; //
int a_verified = a.unverified_safe_because("Error code. App is robust to all values of error code");
The unverified_safe_because
API takes a string argument that allows the
developer to document why they are doing this and why its safe. This string does
not have any special meaning in the code. Rather the RLBox API asks you to
provide a free-form string that acts as documentation. Essentially you are
providing a string that says it is safe to remove the tainting from this type
because... . Such documentation may be useful to other developers who read your
code. The above is equivalent to:
tainted<int> a = ... ; //
int a_verified = a.copy_and_verify([](int val){
// Error code. App is robust to all values of error code
return val;
});
Untainting byte buffers
Sometimes the sandbox returns a buffer that you want to untaint. For example,
you may use a sandboxed XML parse to parse jpeg images. In this example, after
the sandboxed library parses th image, it will produce a byte buffer with image
pixels, probably of the type tainted<char*>
or some other tainted pointer to a
fundamental type.
These types can be untainted with a verifier of the following form
tainted<int*> a = ... ; //
std::unique_ptr<int*> a_verified = a.copy_and_verify_range([&](std::unique_ptr<int*> val){
// <insert security checks here>
return val;
}, size);
This API copies size
bytes out of the sandbox and applies the necessary
checks like ensuring the entire buffer is coming from the sandbox memory.
In the example of a sandboxed image decoder, the buffer may hold completely random data instead of pixels of the image. It is up to you to figure out what your application context is and what security checks need to be in place. In the example of a sandboxed image decoder, the usual expectation is that there is no sensible way to check that decoding has occurred correctly, and rather the rest of your program should be robust to showing an incorrect image on the screen. In this case, there would be no security check in place.
Untainting byte buffers without copying
There maybe some scenarios where the application can handle any possible byte
buffer from the sandbox, i.e., the <insert security checks here>
can be left
empty.
- An example of this, would be if the image being displayed to the app in our sandboxed libjpeg example is say an application background and may not really matter.
In this case, we may want to use the byte before without making a copy, to avoid overheads.
RLBox provides aan API for this called unverified_safe_pointer_because
which
can be used as follows.
tainted<char*> a = ...;
char* raw = a.unverified_safe_pointer_because(10, "Demo of a raw pointer");
unverified_safe_pointer_because
takes two parameters. The first is the number
of bytes in this pointer that you will be accessing. RLBox needs this to ensure
that these many bytes of the pointer stay within the sandbox boundary. The
second is a string, that allows the developer to document why they are doing
this and why its safe. This string does not have any special meaning in the
code. Rather the RLBox API asks you to provide a free-form string that acts as
documentation. Essentially you are providing a string that says it is safe to
remove the tainting from this type because... . Such documentation may be
useful to other developers who read your code.
Untainting C-strings
Untainting C-strings of type tainted<char*>
is covered in the
tutorial.
These types can be untainted with a verifier of the following form
tainted<char*> str = ...;
std::unique_ptr<char[]> checked_string =
str.copy_and_verify_string([](std::unique_ptr<char[]> val) {
// <insert security checks here>
return move(val);
});
The API ensures that the tainted string lives within the sandbox and is null terminated, and makes a copy of the string that you can use.
A useful check in <insert security checks here>
is also to limit the size of
the string you want to allow.
Untainting one-level pointers to fundamental types
If you have a tainted pointer to a fundamental type such as tainted<int*>
,
tainted<float*>
etc., these types can be untainted with a verifier of the
following form
tainted<int*> a = ... ; //
std::unique_ptr<int> a_verified = a.copy_and_verify([](std::unique_ptr<int> val){
// <insert security checks here>
return val;
});
The idea here is that RLBox is effectively creating a deep clone of the object after doing the required checks of ensuring the pointer is in the sandbox. We would ideally allow this API for more types, but C++ makes it hard to know when we can reasonably perform a deep clone of an object, and hence this API is limited to tainted pointers to fundamental types.
This API is also limited to one-level pointers, i.e., things like
tainted<int*>
and is not allowed for tainted<int**>
.
Untainting just the "address bits" of a pointer
Your application may sometimes need just the raw bits of a tainted pointer without needing to look at the data being pointed to. An example of this would be if you want to maintain a hashmap of pointers in the class, but the pointers are produced by the sandbox.
tainted<int*> foo = sandbox.invoke_sandbox_function(...);
std::map<int*, int> my_map;
my_map[foo] = 3; // RLBox gives a compiler error
For this scenario, RLBox provides an API called copy_and_verify_address
which
takes a verifier that accepts a uintptr_t
. This API can be used as follows.
tainted<int*> foo = sandbox.invoke_sandbox_function(...);
std::map<int*, int> my_map;
uintptr_t foo_verified = foo.copy_and_verify_address([&](uintptr_t addr) {
// <insert security checks here>
return addr;
});
my_map[foo_verified] = 3;
Untainting C-arrays of fundamental types
Untainting C-arrays of fundamental types like tainted<int[3]>
is possible
using copy_and_verify
. Note, however, that the API expects the verifier to
accept an argument of std::array
, so that the data is correctly copied.
These types can be untainted with a verifier of the following form
tainted<int[3]> a = ... ; //
std::array<int, 3> a_verified = a.copy_and_verify([](std::array<int, 3> val){
// <insert security checks here>
return val;
});