mirror of
https://github.com/google/sandboxed-api.git
synced 2024-03-22 13:11:30 +08:00
Internal change
PiperOrigin-RevId: 266889781 Change-Id: Ibea87a7bb5fafb50ae3d09f7b0df876beecaf087
This commit is contained in:
parent
da3c6c138e
commit
daa3defac0
|
@ -1,37 +0,0 @@
|
||||||
# Examples
|
|
||||||
|
|
||||||
We have prepared some examples, which might help you to implement your first
|
|
||||||
Sandboxed API library.
|
|
||||||
|
|
||||||
|
|
||||||
## Sum
|
|
||||||
|
|
||||||
A demo library implementing a few [C functions](../examples/sum/lib/sum.c) and a
|
|
||||||
single [C++ function](../examples/sum/lib/sum_cpp.cc).
|
|
||||||
It uses ProtoBuffs to exchange data between host code and the SAPI Library.
|
|
||||||
|
|
||||||
* The sandbox definition can be found in the
|
|
||||||
[sandbox.h](../examples/sum/lib/sandbox.h) file.
|
|
||||||
* The (automatically generated) function annotation file (a file providing
|
|
||||||
prototypes of sandboxed functions) can be found in
|
|
||||||
`bazel-out/genfiles/sandboxed_api/examples/sum/lib/sum-sapi.sapi.h`
|
|
||||||
after a Bazel build.
|
|
||||||
* The actual execution logic (a.k.a. host code) making use of the exported
|
|
||||||
sandboxed procedures can be found in [main_sum.cc](../examples/sum/main_sum.cc).
|
|
||||||
|
|
||||||
|
|
||||||
## zlib
|
|
||||||
|
|
||||||
This is a demo implementation (functional, but currently not used in production)
|
|
||||||
for the zlib library exporting some of its functions, and making them available
|
|
||||||
to the [host code](../examples/zlib/main_zlib.cc).
|
|
||||||
|
|
||||||
The demonstrated functionality of the host code is decoding of zlib streams
|
|
||||||
from stdin to stdout.
|
|
||||||
|
|
||||||
This SAPI library doesn't use the `sandbox.h` file, as it uses the default
|
|
||||||
Sandbox2 policy, and an embedded SAPI library, so there is no need to provide
|
|
||||||
`sapi::Sandbox::GetLibPath()` nor `sapi::Sandbox::GetPolicy()` methods.
|
|
||||||
|
|
||||||
The zlib SAPI can be found in [//sapi_sandbox/examples/zlib](../examples/zlib),
|
|
||||||
along with its [host code](../examples/zlib/main_zlib.cc).
|
|
|
@ -1,76 +0,0 @@
|
||||||
# Host Code
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
The *host code* is the actual code making use of the functionality offered by
|
|
||||||
its contained/isolated/sandboxed counterpart, i.e. a [SAPI Library](library.md).
|
|
||||||
|
|
||||||
Such code implements the logic, that any program making use of a typical library
|
|
||||||
would: it calls functions exported by said library, passing and receiving data
|
|
||||||
to/from it.
|
|
||||||
|
|
||||||
Given that the SAPI Library lives in a separate and contained/sandboxed process,
|
|
||||||
calling such functions directly is not possible. Therefore the SAPI project
|
|
||||||
provides tools which create an API object that proxies accesses to sandboxed
|
|
||||||
libraries.
|
|
||||||
|
|
||||||
More on that can be found under [library](library.md).
|
|
||||||
|
|
||||||
|
|
||||||
## Variables
|
|
||||||
|
|
||||||
In order to make sure that host code can access variables and memory blocks in
|
|
||||||
a remote process, SAPI provides a comprehensive set of C++ classes. These try to
|
|
||||||
make the implementation of the main logic code simpler. To do this you will
|
|
||||||
sometimes have to use those objects instead of typical data types known from C.
|
|
||||||
|
|
||||||
For example, instead of an array of three `int`'s, you will instead have to use
|
|
||||||
and pass to sandboxed functions the following object
|
|
||||||
```cpp
|
|
||||||
int arr[3] = {1, 2, 3};
|
|
||||||
sapi::v::Array<int> sarr(arr, ABSL_ARRAYSIZE(arr));
|
|
||||||
```
|
|
||||||
|
|
||||||
[Read more](variables.md) on the internal data representation used in host
|
|
||||||
code.
|
|
||||||
|
|
||||||
|
|
||||||
## Transactions
|
|
||||||
|
|
||||||
When you use a typical library of functions, you do not have to worry about the
|
|
||||||
fact that a call to a library might fail at runtime, as the linker ensures all
|
|
||||||
necessary functions are available after compilation.
|
|
||||||
|
|
||||||
Unfortunately with the SAPI, the sandboxed library lives in a separate process,
|
|
||||||
therefore we need to check for all kinds of problems related to passing such
|
|
||||||
calls via our RPC layer.
|
|
||||||
|
|
||||||
Users of SAPI need to check - in addition to regular errors returned by the
|
|
||||||
native API of a library - for errors returned by the RPC layer. Sometimes these
|
|
||||||
errors might not be interesting, for example when doing bulk processing and you
|
|
||||||
would just restart the sandbox.
|
|
||||||
|
|
||||||
Handling these errors would mean that each call to a SAPI library is followed
|
|
||||||
by an additional check to RPC layer of SAPI. To make handling of such
|
|
||||||
cases easier we have implemented the `::sapi::Transaction` class.
|
|
||||||
|
|
||||||
This module makes sure that all function calls to the sandboxed library were
|
|
||||||
completed without any RPC-level problems, or it will return relevant error.
|
|
||||||
|
|
||||||
Read more about this module under [Transactions](transactions.md).
|
|
||||||
|
|
||||||
|
|
||||||
## Sandbox restarts
|
|
||||||
|
|
||||||
Many sandboxees handle sensitive user input. This data might be at risk when the
|
|
||||||
sandboxee was corrupted at some point and stores data between runs - imagine
|
|
||||||
an Imagemagick sandbox that starts sending out pictures of the previous run. To
|
|
||||||
avoid this we need to stop reusing sandboxes. This can be achieved by restarting
|
|
||||||
the sandboxee with `::sapi::Sandbox::Restart()` or
|
|
||||||
`::sapi::Transaction::Restart()` when using transactions.
|
|
||||||
|
|
||||||
**Restarting the sandboxee will invalidate any references to the sandboxee!**
|
|
||||||
This means passed file descriptors/allocated memory will not exist anymore.
|
|
||||||
|
|
||||||
Note: Restarting the sandboxee takes some time, about *75-80 ms* on modern
|
|
||||||
machines (more if network namespaces are used).
|
|
|
@ -1,29 +0,0 @@
|
||||||
# How it works
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Sandboxed API project allows to run code of libraries in a sandboxed
|
|
||||||
environment, isolated with the help of [Sandbox2](../sandbox2/README.md).
|
|
||||||
|
|
||||||
Our goal is to provide developers with tools to prepare such libraries for the
|
|
||||||
sandboxing process, as well as necessary APIs to communicate (i.e. make function
|
|
||||||
calls and receive results) with such library.
|
|
||||||
|
|
||||||
All calls to the sandboxed library are passed over our custom RPC implementation
|
|
||||||
to a sandboxed process, and the results are passed back to the caller.
|
|
||||||
|
|
||||||
![SAPI Diagram](images/sapi-overview.png)
|
|
||||||
|
|
||||||
The project also provides [primitives](variables.md) for manual and
|
|
||||||
automatic (based on custom pointer attributes) memory synchronization (arrays,
|
|
||||||
structures) between the SAPI Libraries and the host code.
|
|
||||||
|
|
||||||
A [high-level Transactions API](transactions.md) provides monitoring of SAPI
|
|
||||||
Libraries, and restarts them if they fail (e.g, due to security violations,
|
|
||||||
crashes or resource exhaustion).
|
|
||||||
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
Read our [Get Started](getting-started.md) page to set up your first Sandboxed
|
|
||||||
API project.
|
|
Binary file not shown.
Before Width: | Height: | Size: 105 KiB |
Binary file not shown.
Before Width: | Height: | Size: 40 KiB |
|
@ -1,123 +0,0 @@
|
||||||
# Library
|
|
||||||
|
|
||||||
|
|
||||||
## BUILD.bazel
|
|
||||||
|
|
||||||
Here, you'll prepare a build target, that your [host code](host-code.md)
|
|
||||||
will make use of.
|
|
||||||
|
|
||||||
Start by preparing a `sapi_library()` target in your `BUILD.bazel`
|
|
||||||
file.
|
|
||||||
|
|
||||||
For reference, you can take a peek at a working example from the
|
|
||||||
[zlib example](../examples/zlib/BUILD.bazel).
|
|
||||||
|
|
||||||
```python
|
|
||||||
load(
|
|
||||||
"//sandboxed_api/tools/generator:sapi_generator.bzl",
|
|
||||||
"sapi_library",
|
|
||||||
)
|
|
||||||
|
|
||||||
sapi_library(
|
|
||||||
name = "zlib-sapi",
|
|
||||||
srcs = [], # Extra code compiled with the SAPI library
|
|
||||||
hdrs = [] # Leave empty if embedded SAPI libraries are used, and the
|
|
||||||
# default sandbox policy is sufficient.
|
|
||||||
embed = True, # This is the default
|
|
||||||
functions = [
|
|
||||||
"deflateInit_",
|
|
||||||
"deflate",
|
|
||||||
"deflateEnd",
|
|
||||||
],
|
|
||||||
lib = "@zlib//:zlibonly",
|
|
||||||
lib_name = "Zlib",
|
|
||||||
namespace = "sapi::zlib",
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
* **`name`** - name for your SAPI target
|
|
||||||
* **`srcs`** - any additional sources that you'd like to include with your
|
|
||||||
Sandboxed API library - typically, it's not necessary, unless you want to
|
|
||||||
provide your SAPI Library sandbox definition in a .cc file, and not in the
|
|
||||||
`sandbox.h` file.
|
|
||||||
* **`hdrs`** - as with **`srcs`**. Typically your sandbox definition (sandbox.h)
|
|
||||||
should go here, or empty, if embedded SAPI library is used, and the default
|
|
||||||
sandbox policy is sufficient.
|
|
||||||
* **`functions`** - a list of functions that you'd like to use in your host
|
|
||||||
code. Leaving this list empty will try to export and wrap all functions found
|
|
||||||
in the library.
|
|
||||||
* **`embed`** - whether the SAPI library should be embedded inside host code,
|
|
||||||
so the SAPI Sandbox can be initialized with the
|
|
||||||
`::sapi::Sandbox::Sandbox(FileToc*)` constructor. This is the default.
|
|
||||||
* **`lib`** - (mandatory) the library target you want to sandbox and expose to
|
|
||||||
the host code.
|
|
||||||
* **`lib_name`** - (mandatory) name of the object which is proxying your library
|
|
||||||
functions from the `functions` list. You will call functions from the
|
|
||||||
sandboxed library via this object.
|
|
||||||
* **`input_files`** - list of source files, which SAPI interface generator
|
|
||||||
should scan for library's function declarations. Library's exported headers
|
|
||||||
are always scanned, so `input_files` can usually be left empty.
|
|
||||||
* **`namespace`** - a C++ namespace identifier to place API object defined in
|
|
||||||
`lib_name` into. Defaults to `sapigen`.
|
|
||||||
* **`deps**`** - a list of any additional dependency targets to add. Typically
|
|
||||||
not necessary.
|
|
||||||
* **`header`** - name of the header file to use instead of the generated one.
|
|
||||||
Do not use if you want to auto-generate the code.
|
|
||||||
|
|
||||||
|
|
||||||
## `sapi_library()` Rule Targets
|
|
||||||
|
|
||||||
For the above definition, `sapi_library()` build rule provides the following
|
|
||||||
targets:
|
|
||||||
|
|
||||||
* **`zlib-sapi`** - sandboxed library, substitiution for normal cc_library;
|
|
||||||
consists of **`zlib_sapi.bin`** and sandbox dependencies
|
|
||||||
* **`zlib-sapi.interface`** - generated library interface
|
|
||||||
* **`zlib-sapi.embed`** - `cc_embed_data()` target used to embed sandboxee in
|
|
||||||
the binary. See `bazel/embed_data.bzl`.
|
|
||||||
* **`zlib-sapi.bin`** - sandboxee binary, consists of small communication stub
|
|
||||||
and the library that is being sandboxed.
|
|
||||||
|
|
||||||
|
|
||||||
## Interface Generation
|
|
||||||
|
|
||||||
__`zlib-sapi`__ target creates target library with small communication stub
|
|
||||||
wrapped in [Sandbox2](../sandbox2/README.md). To be able to use the stub and
|
|
||||||
code within the sanbox, you should generate the interface file.
|
|
||||||
|
|
||||||
There are two options:
|
|
||||||
|
|
||||||
1. Add dependency on __`zlib-sapi.interface`__. This will auto-generate a
|
|
||||||
header that you can include in your code - the header name is of the form:
|
|
||||||
__`TARGET_NAME`__`.sapi.h`.
|
|
||||||
2. Run `bazel build TARGET_NAME.interface`, save generated header in your
|
|
||||||
project and include it in the code. You will also need to add the `header`
|
|
||||||
argument to the `sapi_library()` rule to indicate that you will skip code
|
|
||||||
generation.
|
|
||||||
|
|
||||||
|
|
||||||
## Sandbox Description (`sandbox.h`)
|
|
||||||
|
|
||||||
**Note**: If the default SAPI Sandbox policy is sufficient, and the constructor
|
|
||||||
used is **`::sapi::Sandbox::Sandbox(FileToc*)`**, then this file might not be
|
|
||||||
necessary.
|
|
||||||
|
|
||||||
In this step you will prepare the sandbox definition file (typically named
|
|
||||||
`sandbox.h`) for your library.
|
|
||||||
|
|
||||||
The goal of this is to tell the SAPI code where the sandboxed library can be
|
|
||||||
found, and how it should be contained.
|
|
||||||
|
|
||||||
At first, you should tell the SAPI code what your sandboxed library should be
|
|
||||||
allowed to do in terms of security policies and other process constraints. In
|
|
||||||
order to do that, you will have to implement and instantiate an object based on
|
|
||||||
the [::sapi::Sandbox](../sandbox.h) class.
|
|
||||||
|
|
||||||
This object will also specify where your SAPI Library can be found
|
|
||||||
and how it should be executed (though you can depend on default settings).
|
|
||||||
|
|
||||||
A working example of such SAPI object definition file can be found
|
|
||||||
[here](../examples/sum/lib/sandbox.h).
|
|
||||||
|
|
||||||
In order to familiarize yourself with the Sandbox2 policies, you might want to
|
|
||||||
take a look at the [Sandbox2 documenation](../sandbox2/README.md).
|
|
|
@ -1,52 +0,0 @@
|
||||||
# Sandboxing Code
|
|
||||||
|
|
||||||
Sometimes, a piece of code carries a lot of security risk. Examples include:
|
|
||||||
|
|
||||||
* Commercial binary-only code to do document parsing. Document parsing often
|
|
||||||
goes wrong, and binary-only means no opportunity to fix it up.
|
|
||||||
* A web browser's core HTML parsing and rendering. This is such a large amount
|
|
||||||
of code that there will be security bugs.
|
|
||||||
* A JavaScript engine in Java. Accidents here would permit arbitrary calls to
|
|
||||||
Java methods.
|
|
||||||
|
|
||||||
Where a piece of code is very risky, and directly exposed to untrusted users
|
|
||||||
and untrusted input, it is sometimes desirable to sandbox this code. The hardest
|
|
||||||
thing about sandboxing is making the call whether the risk warrants the effort
|
|
||||||
to sandbox.
|
|
||||||
|
|
||||||
There are many approaches to sandboxing, including virtualization, jail
|
|
||||||
environments, network segregation and restricting the permissions code runs
|
|
||||||
with. This page covers technologies available to do the latter: restrict the
|
|
||||||
permission code runs with. See the following depending on which technology you
|
|
||||||
are using:
|
|
||||||
|
|
||||||
## General Sandboxing
|
|
||||||
|
|
||||||
Project/technology | Description
|
|
||||||
-------------------------------------------|------------
|
|
||||||
[Sandbox2](../sandbox2/README.md) | Linux sandboxing using namespaces, resource limits and seccomp-bpf syscall filters. Provides the underlying sandboxing technology for Sandboxed API.
|
|
||||||
[gVisor](https://github.com/google/gvisor) | Uses hardware virtualization and a small syscall emulation layer implemented in Go.
|
|
||||||
|
|
||||||
|
|
||||||
## Sandbox command-line tools
|
|
||||||
|
|
||||||
Project/technology | Description
|
|
||||||
---------------------|------------
|
|
||||||
[Firejail](https://github.com/netblue30/firejail) | Lightweight sandboxing tool implemented as a SUID program with minimal dependencies.
|
|
||||||
[Minijail](https://android.googlesource.com/platform/external/minijail/) | The sandboxing and containment tool used in Chrome OS and Android. Provides an executable and a library that can be used to launch and sandbox other programs and code.
|
|
||||||
[NSJail](nsjail.com) | Process isolation for Linux using namespaces, resource limits and seccomp-bpf syscall filters. Can optionally make use of [Kafel](https://github.com/google/kafel/), a custom domain specific language, for specifying syscall policies.
|
|
||||||
|
|
||||||
|
|
||||||
## C/C++
|
|
||||||
|
|
||||||
Project/technology | Description
|
|
||||||
-------------------------|------------
|
|
||||||
[Sandboxed API](..) | Reusable sandboxes for C/C++ libraries using Sandbox2.
|
|
||||||
(Portable) Native Client | **(Deprecated)** Powerful technique to sandbox C/C++ binaries by compiling to a restricted subset of x86 (NaCl)/LLVM bytecode (PNaCl).
|
|
||||||
|
|
||||||
|
|
||||||
## Graphical/Desktop Applications
|
|
||||||
|
|
||||||
Project/technology | Description
|
|
||||||
----------------------------------------------|------------
|
|
||||||
[Flatpak](https://github.com/flatpak/flatpak) | Built on top of [Bubblewrap](https://github.com/projectatomic/bubblewrap), provides sandboxing for Linux desktop applications. Puts an emphasis on packaging and distribution of native apps.
|
|
|
@ -1,133 +0,0 @@
|
||||||
# Transactions
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
When using SAPI, there is another layer around library calls that might
|
|
||||||
fail, which is why all library function prototypes return `::sapi::StatusOr<T>`
|
|
||||||
instead of `T`. In the event that the library function invocation fails (e.g.
|
|
||||||
because of a sandbox violation), the return value will contain details about
|
|
||||||
the error that occurred.
|
|
||||||
|
|
||||||
In order to deal with those exceptional situations, the high-level
|
|
||||||
`::sapi::Transaction` module can be used.
|
|
||||||
|
|
||||||
|
|
||||||
### `::sapi::Transaction`
|
|
||||||
|
|
||||||
With SAPI we are trying to isolate the [host code](host-code.md) from such
|
|
||||||
problems in the sandboxed library, giving ability to the caller to restart or
|
|
||||||
abort the problematic data processing request.
|
|
||||||
The transaction class goes one step further and automatically repeats processes
|
|
||||||
that have failed.
|
|
||||||
|
|
||||||
The usual pattern when dealing with libraries looks like this:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
LibInit();
|
|
||||||
while (data = NextDataToProcess()) {
|
|
||||||
result += LibProcessData(data);
|
|
||||||
}
|
|
||||||
LibClose();
|
|
||||||
```
|
|
||||||
|
|
||||||
This translates to this code when using SAPI:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
::sapi::Status Init(::sapi::Sandbox* sandbox) {
|
|
||||||
LibraryAPI lib(sandbox);
|
|
||||||
SAPI_RETURN_IF_ERROR(lib.LibInit());
|
|
||||||
return ::sapi::OkStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
::sapi::Status Finish(::sapi::Sandbox *sandbox) {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
::sapi::Status handle_data(::sapi::Sandbox *sandbox, Data data_to_process,
|
|
||||||
Result *out) {
|
|
||||||
LibraryAPI lib(sandbox);
|
|
||||||
SAPI_ASSIGN_OR_RETURN(*out, lib.LibProcessData(data_to_process));
|
|
||||||
return ::sapi::OkStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
void handle() {
|
|
||||||
// ...
|
|
||||||
::sapi::BasicTransaction transaction(Init, Finish);
|
|
||||||
while (data = NextDataToProcess()) {
|
|
||||||
::sandbox2::Result result;
|
|
||||||
transaction.Run(handle_data, data, &result);
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The transaction class makes sure to reinitialize the library in the case that an
|
|
||||||
error occures during the `handle_data` invovcation - more on this later.
|
|
||||||
|
|
||||||
SAPI transaction can be used in two different ways, depending on your
|
|
||||||
requirements:
|
|
||||||
|
|
||||||
* Implementing a transaction class inheriting from `::sapi::Transaction`,
|
|
||||||
* Using function pointers passed to `::sapi::BasicTransaction`, see above.
|
|
||||||
|
|
||||||
Both methods allow you to specify the following three functions:
|
|
||||||
|
|
||||||
* `::sapi::Transaction::Init()`, which will be called **only once** during each
|
|
||||||
transaction to the sandboxed library (and, also, during each restart of the
|
|
||||||
transaction). It's similar to calling a `LibInit()` function from a typical
|
|
||||||
C/C++ library.
|
|
||||||
* `::sapi::Transaction::Main()`, which will be called for each call to
|
|
||||||
`::sapi::Transaction::Run()`.
|
|
||||||
* `::sapi::Transaction::Finish()`, which will be called during the
|
|
||||||
`::sapi::Transaction` object destruction, resembling the call to a typical
|
|
||||||
`LibClose()` function call.
|
|
||||||
|
|
||||||
### Transaction Restarts
|
|
||||||
|
|
||||||
If any kind of problem arises during execution of the
|
|
||||||
`Init()`/`Main()`/`Finish()` methods, e.g, they return a failure return code due
|
|
||||||
to library error, or sandboxed process crash, or a security sandbox violation,
|
|
||||||
the transaction will be restarted (by default, `kDefaultRetryCnt` times, see
|
|
||||||
[transaction.h](../transaction.h)).
|
|
||||||
|
|
||||||
During such restarts the `Init()`/`Main()` flow is observed (i.e, the `Init()`
|
|
||||||
function is called again), and if repeated calls to the
|
|
||||||
`::sapi::Transaction::Run()` method return errors, then the whole method
|
|
||||||
returns an error to its caller.
|
|
||||||
|
|
||||||
### Sandbox/RPC Error handling
|
|
||||||
|
|
||||||
Although the automatically generated [SAPI library
|
|
||||||
interface](library.md#Interface-Generation) tries to be as similar to the
|
|
||||||
original library function prototype we somehow need to signal Sandbox/RPC
|
|
||||||
errors. Instead of providing the return value directly, SAPI makes use of
|
|
||||||
`::sapi::StatusOr<T>` for return types `T` != `void` or `::sapi::Status` for
|
|
||||||
functions returning `void`.
|
|
||||||
|
|
||||||
Example of how to use the API (from the sum example):
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
::sapi::Status SumTransaction::Main() {
|
|
||||||
SumApi f(GetSandbox());
|
|
||||||
// ::sapi::StatusOr<int> sum(int a, int b)
|
|
||||||
SAPI_ASSIGN_OR_RETURN(int v, f.sum(1000, 337));
|
|
||||||
...
|
|
||||||
// ::sapi::Status sums(sapi::v::Ptr* params)
|
|
||||||
SumParams params;
|
|
||||||
params.mutable_data()->a = 1111;
|
|
||||||
params.mutable_data()->b = 222;
|
|
||||||
params.mutable_data()->ret = 0;
|
|
||||||
SAPI_RETURN_IF_ERROR(f.sums(params.PtrBoth()));
|
|
||||||
...
|
|
||||||
int *ssaddr;
|
|
||||||
SAPI_RETURN_IF_ERROR(GetSandbox()->Symbol(
|
|
||||||
"sumsymbol", reinterpret_cast<void**>(&ssaddr)));
|
|
||||||
::sapi::v::Int sumsymbol;
|
|
||||||
sumsymbol.SetRemote(ssaddr);
|
|
||||||
SAPI_RETURN_IF_ERROR(GetSandbox()->TransferFromSandboxee(&sumsymbol));
|
|
||||||
...
|
|
||||||
return ::sapi::OkStatus();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
# Variables
|
|
||||||
|
|
||||||
Typically, you'll be able to use native C-types to deal with the SAPI Library,
|
|
||||||
but sometimes some special types will be required. This mainly happens when
|
|
||||||
passing pointers to simple types, and pointers to memory blocks (structures,
|
|
||||||
arrays). Because you operate on local process memory (of the host code), when
|
|
||||||
calling a function taking a pointer, it must be converted into a corresponding
|
|
||||||
pointer inside the sandboxed process (SAPI Library) memory.
|
|
||||||
|
|
||||||
Take a look at the [SAPI directory](..). The `var_*.h` files provide classes
|
|
||||||
and templates representing various types of data, e.g. `::sapi::v::UChar`
|
|
||||||
represents well-known `unsigned char` while `::sapi::v::Array<int>` represents
|
|
||||||
an array of integers (`int[]`).
|
|
||||||
|
|
||||||
|
|
||||||
## Pointers
|
|
||||||
|
|
||||||
When creating your host code, you'll be generally using functions exported by
|
|
||||||
an auto-generated SAPI interface header file from your SAPI Library. Most of
|
|
||||||
them will take simple types (or typedef'd types), but when a pointer is needed,
|
|
||||||
you need to wrap it with the `::sapi::v::Ptr` template class.
|
|
||||||
|
|
||||||
Most types that you will use, provide the following methods:
|
|
||||||
|
|
||||||
* `::PtrNone()`: this pointer, when passed to the SAPI Library function,
|
|
||||||
doesn't synchronize the underlying memory between the host code process and
|
|
||||||
the SAPI Library process.
|
|
||||||
* `::PtrBefore()`: when passed to the SAPI Library function, will synchronize
|
|
||||||
memory of the object it points to, before the call takes place. This means,
|
|
||||||
that the local memory of the pointed variable will be transferred to the
|
|
||||||
SAPI Library process before the call is initiated.
|
|
||||||
* `::PtrAfter()`: this pointer will synchronize memory of the object it points
|
|
||||||
to, after the call has taken place. This means, that the remote memory of a
|
|
||||||
pointed variable will be transferred to the host code process' memory, after
|
|
||||||
the call has been completed.
|
|
||||||
* `::PtrBoth()`: combines the functionality of both `::PtrBefore()` and
|
|
||||||
`::PtrAfter()`
|
|
||||||
|
|
||||||
|
|
||||||
## Structures
|
|
||||||
|
|
||||||
When a pointer to a structure is used inside a call to a SAPI Library, that
|
|
||||||
structure needs to created with the `::sapi::v::Struct` template. You can use
|
|
||||||
the `PtrNone()`/`Before()`/`After()`/`Both()` methods of this template to obtain
|
|
||||||
a relevant `::sapi::v::Ptr` object that can be used in SAPI Library function
|
|
||||||
calls.
|
|
||||||
|
|
||||||
|
|
||||||
## Arrays
|
|
||||||
|
|
||||||
The `::sapi::v::Array` template allow to wrap both existing arrays of elements,
|
|
||||||
as well as dynamically create one for you (please take a look at its
|
|
||||||
constructor to decide which one you would like to use).
|
|
||||||
|
|
||||||
The use of pointers is analogous to [Structures](#structures).
|
|
||||||
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
Our canonical [sum library](../examples/sum/main_sum.cc) demonstrates use of
|
|
||||||
pointers to call sandboxed functions in its corresponding SAPI Library.
|
|
||||||
|
|
||||||
You might also want to take a look at the [Examples](examples.md) page to
|
|
||||||
familiarize yourself with other working examples of libraries sandboxed
|
|
||||||
with SAPI.
|
|
||||||
|
|
||||||
* [sum library](../examples/sum/main_sum.cc)
|
|
||||||
* [stringop](../examples/stringop/main_stringop.cc)
|
|
||||||
* [zlib](../examples/zlib/main_zlib.cc)
|
|
|
@ -1,119 +0,0 @@
|
||||||
# Examples
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
We have prepared a few examples to demonstrate how to use sandbox2 depending on
|
|
||||||
your situation and how to write policies.
|
|
||||||
|
|
||||||
You can find them in [//sandboxed_api/sandbox2/examples](../examples), read on
|
|
||||||
for detailed explanations.
|
|
||||||
|
|
||||||
## CRC4
|
|
||||||
|
|
||||||
The CRC4 example is an intentionally buggy calculation of a CRC4 checksum, it
|
|
||||||
demonstrates how to sandbox another program and how to communicate with it.
|
|
||||||
|
|
||||||
* [crc4bin.cc](../examples/crc4/crc4bin.cc): is the program we want to sandbox
|
|
||||||
(the *sandboxee*)
|
|
||||||
* [crc4sandbox.cc](../examples/crc4/crc4sandbox.cc): is the sandbox program that
|
|
||||||
will run it (the *executor*).
|
|
||||||
|
|
||||||
How it works:
|
|
||||||
1. The *executor* starts the *sandboxee* from its file path using
|
|
||||||
`::sandbox2::GetDataDependencyFilePath()`.
|
|
||||||
2. The *executor* sends input to the *sandboxee* over the communication channel
|
|
||||||
`Comms` using `SendBytes()`.
|
|
||||||
3. The *sandboxee* calculates the CRC4 and sends its replies back to the
|
|
||||||
*executor* over the communication channel `Comms` which receives it with
|
|
||||||
`RecvUint32()`.
|
|
||||||
|
|
||||||
If the program makes any other syscall other than communicating (`read()` and
|
|
||||||
`write()`), it is killed for policy violation.
|
|
||||||
|
|
||||||
|
|
||||||
## static
|
|
||||||
|
|
||||||
The static example demonstrates how to sandbox a statically linked binary, such
|
|
||||||
as a third-party binary for which you do not have the source, so is not aware
|
|
||||||
that it will be sandboxed.
|
|
||||||
|
|
||||||
* [static_bin.cc](../examples/static/static_bin.cc): the *sandboxee* is a
|
|
||||||
static C binary that converts ASCII text from standard input to uppercase.
|
|
||||||
* [static_sandbox.cc](../examples/static/static_sandbox.cc): the *executor*
|
|
||||||
with its policy, limits and using a file descriptor for *sandboxee* input.
|
|
||||||
|
|
||||||
How it works:
|
|
||||||
|
|
||||||
1. The *executor* starts the *sandboxee* from its file path using
|
|
||||||
`GetDataDependencyFilepath`, just like for **CRC4**.
|
|
||||||
2. It sets up limits, opens a file descriptor on `/proc/version` and marks it
|
|
||||||
to be mapped in the *sandboxee* with `MapFd`.
|
|
||||||
3. The policy allows some syscalls (`open`) to return an error (`ENOENT`),
|
|
||||||
rather than being killed for policy violation. This can be useful when
|
|
||||||
sandboxing a third party program where we cannot modify which syscalls are
|
|
||||||
made, but we can make them fail gracefully.
|
|
||||||
|
|
||||||
## tool
|
|
||||||
|
|
||||||
The tool example is both a tool to develop your own policies and experiment with
|
|
||||||
**sandbox2** APIs as well a demonstration of its features.
|
|
||||||
|
|
||||||
* [sandbox2tool.cc](..examples/tool/sandbox2tool.cc): the *executor*
|
|
||||||
demonstrating
|
|
||||||
* how to run another binary sandboxed,
|
|
||||||
* how to set up filesystem checks, and
|
|
||||||
* how the *executor* can run the *sandboxee* asynchronously to read its
|
|
||||||
output progressively
|
|
||||||
|
|
||||||
Try it yourself:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bazel run //sandboxed_api/sandbox2/examples/tool:sandbox2tool -- \
|
|
||||||
/bin/cat /etc/hostname
|
|
||||||
```
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
|
|
||||||
* `--sandbox2tool_keep_env` to keep current environment variables
|
|
||||||
* `--sandbox2tool_redirect_fd1` to receive the *sandboxee* STDOUT_FILENO (1)
|
|
||||||
and output it locally
|
|
||||||
* `--sandbox2tool_cpu_timeout` to set CPU timeout in seconds
|
|
||||||
* `--sandbox2tool_walltime_timeout` to set wall-time timeout in seconds
|
|
||||||
* `--sandbox2tool_file_size_creation_limit` to set the maximum size of created
|
|
||||||
files
|
|
||||||
* `--sandbox2tool_cwd` to set sandbox current working directory
|
|
||||||
|
|
||||||
## custom_fork
|
|
||||||
|
|
||||||
The custom_fork example demonstrates how to create a sandbox, which will
|
|
||||||
initialize the binary, and then wait for `fork()` requests coming from the
|
|
||||||
parent executor.
|
|
||||||
|
|
||||||
This mode offers potentially increased performance with regard to other types of
|
|
||||||
sandboxing, as here, creating new instances of sandboxees doesn't require
|
|
||||||
executing new binaries, just fork()-ing the existing ones
|
|
||||||
|
|
||||||
* [custom_fork_bin.cc](../examples/custom_fork): is the custom fork-server,
|
|
||||||
receiving requests to `fork()` (via `Client::WaitAndFork`) in order to spawn
|
|
||||||
new sandboxees
|
|
||||||
* [custom_fork_sandbox.cc](../examples/custom_fork/custom_fork_sandbox.cc): is
|
|
||||||
the executor, which starts a custom fork server. Then it sends requests to it
|
|
||||||
(via new executors) to spawn (via `fork()`) new sandboxees.
|
|
||||||
|
|
||||||
## network
|
|
||||||
|
|
||||||
Enabling the network namespace prevents the sandboxed process from connecting to
|
|
||||||
the outside world. This example demonstrates how to deal with this problem.
|
|
||||||
|
|
||||||
Namespaces are enabled when either
|
|
||||||
`::sandbox2::PolicyBuilder::EnableNamespaces()` is called, or some other
|
|
||||||
function that enables namespaces like `AddFile()`. To deal with this problem,
|
|
||||||
we can initialize a connection inside the executor and pass the socket file
|
|
||||||
descriptor via `::sandbox2::Comms::SendFD()`. The sandboxee receives the socket
|
|
||||||
by using `::sandbox2::Comms::RecvFD()` and then it can use this socket to
|
|
||||||
exchange the data as usual.
|
|
||||||
|
|
||||||
* [network_bin.cc](examples/network/network_bin.cc): is the program we want to
|
|
||||||
sandbox (the sandboxee).
|
|
||||||
* [network_sandbox.cc](examples/network/network_sandbox.cc): is the sandbox
|
|
||||||
program that will run it (the executor).
|
|
|
@ -1,123 +0,0 @@
|
||||||
# FAQ
|
|
||||||
|
|
||||||
## Can I use threads?
|
|
||||||
|
|
||||||
Yes, threads are supported in sandbox2.
|
|
||||||
|
|
||||||
### All threads must be sandboxed
|
|
||||||
|
|
||||||
Because of the way Linux works, the seccomp-bpf policy is applied to the current
|
|
||||||
thread only: this means other existing threads do not get the policy, but future
|
|
||||||
threads will inherit the policy.
|
|
||||||
|
|
||||||
If you are using sandbox2 in the
|
|
||||||
[default mode](getstarted.md#a-Execute-a-binary-with-sandboxing-already-enabled)
|
|
||||||
where sandboxing is enabled before `execve()`, all threads will inherit the
|
|
||||||
policy, and there is no problem. This is the preferred mode of sandboxing.
|
|
||||||
|
|
||||||
If you want to use the
|
|
||||||
[second mode](getstarted.md#b-Tell-the-executor-when-to-be-sandboxed) where the
|
|
||||||
executor has
|
|
||||||
`set_enable_sandbox_before_exec(false)` and the sandboxee tells the executor
|
|
||||||
when it wants to be sandboxed with `SandboxMeHere()`, then the filter still
|
|
||||||
needs to be applied to all threads. Otherwise, there is a risk of a sandbox
|
|
||||||
escape: malicious code could migrate from a sandboxed thread to an unsandboxed
|
|
||||||
thread.
|
|
||||||
|
|
||||||
The Linux kernel introduced the TSYNC flag in version 3.17, which allows
|
|
||||||
applying a policy to all threads. Before this flag, it was only possible to
|
|
||||||
apply the policy on a thread-by-thread basis.
|
|
||||||
|
|
||||||
If sandbox2 detects that it is running on a kernel without TSYNC-support and you
|
|
||||||
call `SandboxMeHere()` from multi-threaded program, sandbox2 will abort, since
|
|
||||||
this would compromise the safety of the sandbox.
|
|
||||||
|
|
||||||
## How should I compile my sandboxee?
|
|
||||||
|
|
||||||
If not careful, it is easy to inherit a lot of dependencies and side effects
|
|
||||||
(extra syscalls, file accesses or even network connections) which make
|
|
||||||
sandboxing harder (tracking down all side effects) and less safe (because the
|
|
||||||
syscall and file policies are wider). Some compile options can help reduce this:
|
|
||||||
|
|
||||||
* statically compile the sandboxee binary to avoid dynamic linking which uses a
|
|
||||||
lot of syscalls (`open()`/`openat()`, `mmap()`, etc.). Also since Bazel adds
|
|
||||||
`pie` by default but static is incompatible with it, use the features flag to
|
|
||||||
force it off.
|
|
||||||
That is, use the following options in
|
|
||||||
[cc_binary](https://docs.bazel.build/versions/master/be/c-cpp.html#cc_binary)
|
|
||||||
rules:
|
|
||||||
|
|
||||||
```python
|
|
||||||
linkstatic = 1,
|
|
||||||
features = [
|
|
||||||
"fully_static_link", # link libc statically
|
|
||||||
"-pie",
|
|
||||||
],
|
|
||||||
```
|
|
||||||
|
|
||||||
*However:* this has the downside of reducing ASLR heap entropy (from 30 bits
|
|
||||||
to 8 bits), making exploits easier. Decide carefully what is preferable
|
|
||||||
depending on your sandbox implementation and policy:
|
|
||||||
|
|
||||||
* **not static**: good heap ASLR, potentially harder to get initial code
|
|
||||||
execution but at the cost of a less effective sandbox policy, potentially
|
|
||||||
easier to break out of.
|
|
||||||
* **static**: bad heap ASLR, potentially easier to get initial code execution
|
|
||||||
but a more effective sandbox policy, potentially harder to break out of.
|
|
||||||
|
|
||||||
It is an unfortunate choice to make because the compiler does not support
|
|
||||||
static PIE (Position Independent Executables). PIE is implemented by having
|
|
||||||
the binary be a dynamic object, and the dynamic loader maps it at a random
|
|
||||||
location before executing it. Then because the heap is traditionnally placed
|
|
||||||
at a random offset after the base address of the binary (and expanded with
|
|
||||||
`brk` syscall), it means for static binaries the heap ASLR entropy is only
|
|
||||||
this offset because there is no PIE.
|
|
||||||
|
|
||||||
For examples of these compiling options, look at the
|
|
||||||
[static](examples.md#static) example
|
|
||||||
[BUILD.bazel](../examples/static/BUILD.bazel): `static_bin.cc` is compiled
|
|
||||||
statically, which allows us to have a very tight syscall policy. This works
|
|
||||||
nicely for sandboxing third party binaries too.
|
|
||||||
|
|
||||||
## Can I sandbox 32-bit x86 binaries?
|
|
||||||
|
|
||||||
Sandbox2 can only sandbox the same arch as it was compiled with.
|
|
||||||
|
|
||||||
In addition, support for 32-bit x86 has been removed from Sandbox2. If you try
|
|
||||||
to use a 64-bit x86 executor to sandbox a 32-bit x86 binary, or a 64-bit x86
|
|
||||||
binary making 32-bit syscalls (via `int 0x80`), both will generate a sandbox
|
|
||||||
violation that can be identified with the architecture label *[X86-32]*.
|
|
||||||
|
|
||||||
The reason behind this behavior is that syscall numbers are different between
|
|
||||||
architectures and since the syscall policy is written in the architecture of the
|
|
||||||
executor, it would be dangerous to allow a different architecture for the
|
|
||||||
sandboxee. Indeed, allowing an seemingly harmless syscall that in fact means
|
|
||||||
another more harmful syscall could open up the sandbox to an escape.
|
|
||||||
|
|
||||||
## Any limits on the number of sandboxes an executor process can request?
|
|
||||||
|
|
||||||
For each sandboxee instance (new process spawned from the forkserver) a new
|
|
||||||
thread is created - that's where the limitation would lie.
|
|
||||||
|
|
||||||
## Can an Executor request the creation of more than one Sandbox?
|
|
||||||
|
|
||||||
No. There is a 1:1 correspondence - an `Executor` instance stores the PID of the
|
|
||||||
sandboxee, manages the `Comms` instance to the `Sandbox` instance, etc.
|
|
||||||
|
|
||||||
## Can I use sandbox2 from Go?
|
|
||||||
|
|
||||||
Yes. Write your executor in C++ and expose it to Go via SWIG.
|
|
||||||
|
|
||||||
## Why do I get `Function not implemented` inside `forkserver.cc?`
|
|
||||||
|
|
||||||
Sandbox2 only supports running on reasonably new kernels. Our current cut-off is
|
|
||||||
the 3.19 kernel though that might change in the future. The reason for this is
|
|
||||||
that we are using relatively new kernel features including user namespaces and
|
|
||||||
seccomp with the TSYNC flag.
|
|
||||||
|
|
||||||
If you are running on prod, this should not be in issue, since almost the entire
|
|
||||||
fleet is running a new enough kernel. If you have any issues with this, please
|
|
||||||
contact us.
|
|
||||||
|
|
||||||
If you are running on Debian or Ubuntu, updating your kernel is as easy as
|
|
||||||
`apt-get install linux-image-[recent version]`.
|
|
|
@ -1,356 +0,0 @@
|
||||||
# Getting started with Sandbox2
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
In this guide, you will learn how to create your own sandbox, policy and tweaks.
|
|
||||||
It is meant as a guide, alongside the [examples](examples.md) and code
|
|
||||||
documentation in the header files.
|
|
||||||
|
|
||||||
|
|
||||||
## 1. Choose an executor
|
|
||||||
|
|
||||||
Sandboxing starts with an *executor* (see [How it works](howitworks.md)), which
|
|
||||||
will be responsible for running the *sandboxee*. The API for this is in
|
|
||||||
[executor.h](../executor.h). It is very flexible to let you choose what works
|
|
||||||
best for your use case.
|
|
||||||
|
|
||||||
### a. Execute a binary with sandboxing already enabled
|
|
||||||
|
|
||||||
This is the simplest and safest way to use sandboxing. For examples see
|
|
||||||
[static](examples.md#static) and [sandboxed tool](examples.md#tool).
|
|
||||||
|
|
||||||
```c++
|
|
||||||
#include "sandboxed_api/sandbox2/executor.h"
|
|
||||||
|
|
||||||
std::string path = "path/to/binary";
|
|
||||||
std::vector<std::string> args = {path}; // args[0] will become the sandboxed
|
|
||||||
// process' argv[0], typically the
|
|
||||||
// path to the binary.
|
|
||||||
auto executor = absl::make_unique<sandbox2::Executor>(path, args);
|
|
||||||
```
|
|
||||||
|
|
||||||
### b. Tell the executor when to be sandboxed
|
|
||||||
|
|
||||||
This offers you the flexibility to be unsandboxed during initialization, then
|
|
||||||
choose when to enter sandboxing by calling
|
|
||||||
`::sandbox2::Client::SandboxMeHere()`. The code has to be careful to always
|
|
||||||
call this or it would be unsafe to proceed, and it has to be single-threaded
|
|
||||||
(read why in the [FAQ](faq.md#Can-I-use-threads)). For an example see
|
|
||||||
[crc4](examples.md#CRC4).
|
|
||||||
|
|
||||||
Note: The [filesystem restrictions](#Filesystem-checks) will be in effect right
|
|
||||||
from the start of your sandboxee. Using this mode allows you to enable the
|
|
||||||
syscall filter later on from the sandboxee.
|
|
||||||
|
|
||||||
```c++
|
|
||||||
#include "sandboxed_api/sandbox2/executor.h"
|
|
||||||
|
|
||||||
std::string path = "path/to/binary";
|
|
||||||
std::vector<std::string> args = {path};
|
|
||||||
auto executor = absl::make_unique<sandbox2::Executor>(path, args);
|
|
||||||
executor->set_enable_sandbox_before_exec(false);
|
|
||||||
```
|
|
||||||
|
|
||||||
### c. Prepare a binary, wait for fork requests, and sandbox on your own
|
|
||||||
|
|
||||||
This mode allows you to start a binary, prepare it for sandboxing, and - at the
|
|
||||||
specific moment of your binary's lifecycle - make it available for the
|
|
||||||
executor. The executor will send fork request to your binary, which will
|
|
||||||
`fork()` (via `::sandbox2::ForkingClient::WaitAndFork()`). The newly created
|
|
||||||
process will be ready to be sandboxed with
|
|
||||||
`::sandbox2::Client::SandboxMeHere()`. This mode comes with a few downsides,
|
|
||||||
however: For example, it pulls in more dependencies in your sandboxee and
|
|
||||||
does not play well with namespaces, so it is only recommended it if you have
|
|
||||||
tight performance requirements.
|
|
||||||
|
|
||||||
For an example see [custom_fork](examples.md#custom_fork).
|
|
||||||
|
|
||||||
```c++
|
|
||||||
#include "sandboxed_api/sandbox2/executor.h"
|
|
||||||
|
|
||||||
// Start the custom ForkServer
|
|
||||||
std::string path = "path/to/binary";
|
|
||||||
std::vector<std::string> args = {path};
|
|
||||||
auto fork_executor = absl::make_unique<sandbox2::Executor>(path, args);
|
|
||||||
fork_executor->StartForkServer();
|
|
||||||
|
|
||||||
// Initialize Executor with Comms channel to the ForkServer
|
|
||||||
auto executor = absl::make_unique<sandbox2::Executor>(
|
|
||||||
fork_executor->ipc()->GetComms());
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Creating a policy
|
|
||||||
|
|
||||||
Once you have an executor you need to define the policy for the sandboxee: this
|
|
||||||
will restrict the syscalls and arguments that the sandboxee can make as well as
|
|
||||||
the files it can access. For instance, a policy could allow `read()` on a given
|
|
||||||
file descriptor (e.g. `0` for stdin) but not another.
|
|
||||||
|
|
||||||
To create a [policy object][filter], use the
|
|
||||||
[PolicyBuilder](../policybuilder.h). It comes with helper functions that allow
|
|
||||||
many common operations (such as `AllowSystemMalloc()`), whitelist syscalls
|
|
||||||
(`AllowSyscall()`) or grant access to files (`AddFile()`).
|
|
||||||
|
|
||||||
If you want to restrict syscall arguments or need to perform more complicated
|
|
||||||
checks, you can specify a raw seccomp-bpf filter using the bpf helper macros
|
|
||||||
from the Linux kernel. See the [kernel documentation][filter] for more
|
|
||||||
information about BPF. If you find yourself writing repetitive BPF-code that
|
|
||||||
you think should have a usability-wrapper, feel free to file a feature request.
|
|
||||||
|
|
||||||
Coming up with the syscalls to whitelist is still a bit of manual work
|
|
||||||
unfortunately. Create a policy with the syscalls you know your binary needs and
|
|
||||||
run it with a common workload. If a violation gets triggered, whitelist the
|
|
||||||
syscall and repeat the process. If you run into a violation that you think might
|
|
||||||
be risky to whitelist and the program handles errors gracefullly, you can try to
|
|
||||||
make it return an error instead with `BlockSyscallWithErrno()`.
|
|
||||||
|
|
||||||
[filter]: https://www.kernel.org/doc/Documentation/networking/filter.txt
|
|
||||||
|
|
||||||
```c++
|
|
||||||
#include "sandboxed_api/sandbox2/policy.h"
|
|
||||||
#include "sandboxed_api/sandbox2/policybuilder.h"
|
|
||||||
#include "sandboxed_api/sandbox2/util/bpf_helper.h"
|
|
||||||
|
|
||||||
std::unique_ptr<sandbox2::Policy> CreatePolicy() {
|
|
||||||
return sandbox2::PolicyBuilder()
|
|
||||||
.AllowSyscall(__NR_read) // See also AllowRead()
|
|
||||||
.AllowTime() // Allow time, gettimeofday and clock_gettime
|
|
||||||
.AddPolicyOnSyscall(__NR_write, {
|
|
||||||
ARG(0), // fd is the first argument of write (argument #0)
|
|
||||||
JEQ(1, ALLOW), // allow write only on fd 1
|
|
||||||
KILL, // kill if not fd 1
|
|
||||||
})
|
|
||||||
.AddPolicyOnSyscall(__NR_mprotect, {
|
|
||||||
ARG_32(2), // prot is a 32-bit wide argument, so it's OK to use *_32
|
|
||||||
// macro here
|
|
||||||
JNE32(PROT_READ | PROT_WRITE, KILL), // prot must be the RW, otherwise
|
|
||||||
// kill the process
|
|
||||||
ARG(1), // len is a 64-bit argument
|
|
||||||
JNE(0x1000, KILL), // Allow single page syscalls only, otherwise kill
|
|
||||||
// the process
|
|
||||||
ALLOW, // Allow for the syscall to proceed, if prot and
|
|
||||||
// size match
|
|
||||||
})
|
|
||||||
// Allow the open() syscall but always return "not found".
|
|
||||||
.BlockSyscallWithErrno(__NR_open, ENOENT)
|
|
||||||
.BuildOrDie();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Tip: Test for the most used syscalls at the beginning so you can allow them
|
|
||||||
early without consulting the rest of the policy.
|
|
||||||
|
|
||||||
|
|
||||||
### Filesystem checks
|
|
||||||
|
|
||||||
The default way to grant access to files is by using the `AddFile()` class of
|
|
||||||
functions of the `PolicyBuilder`. This will automatically enable user namespace
|
|
||||||
support that allows us to create a custom chroot for the sandboxee and gives you
|
|
||||||
some other features such as creating tmpfs mounts.
|
|
||||||
|
|
||||||
```c++
|
|
||||||
sandbox2::PolicyBuilder()
|
|
||||||
// ...
|
|
||||||
.AddFile("/etc/localtime")
|
|
||||||
.AddDirectory("/usr/share/fonts")
|
|
||||||
.AddTmpfs("/tmp")
|
|
||||||
.BuildOrDie();
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Adjusting limits
|
|
||||||
|
|
||||||
Sandboxing by restricting syscalls is one thing, but if the job can run
|
|
||||||
indefinitely or exhaust RAM and other resources that is not good either.
|
|
||||||
Therefore, by default the sandboxee runs under tight execution limits, which can
|
|
||||||
be adjusted using the [Limits](../limits.h) class, available by calling
|
|
||||||
`limits()` on the `Executor` object created earlier. For an example see [sandbox
|
|
||||||
tool](examples.md#tool).
|
|
||||||
|
|
||||||
```c++
|
|
||||||
// Restrict the address space size of the sandboxee to 4 GiB.
|
|
||||||
executor->limits()->set_rLimit_as(4ULL << 30);
|
|
||||||
// Kill sandboxee with SIGXFSZ if it writes more than 1 GiB to the filesystem.
|
|
||||||
executor->limits()->set_rLimit_fsize(1ULL << 30);
|
|
||||||
// Number of file descriptors which can be used by the sandboxee.
|
|
||||||
executor->limits()->set_rLimit_nofile(1ULL << 10);
|
|
||||||
// The sandboxee is not allowed to create core files.
|
|
||||||
executor->limits()->set_rLimit_core(0);
|
|
||||||
// Maximum 300s of real CPU time.
|
|
||||||
executor->limits()->set_rLimit_cpu(300);
|
|
||||||
// Maximum 120s of wall time.
|
|
||||||
executor->limits()->set_walltime_limit(absl::Seconds(120));
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Running the sandboxee
|
|
||||||
|
|
||||||
With our executor and policy ready, we can now create the `Sandbox2` object and
|
|
||||||
run it synchronously. For an example see [static](examples.md#static).
|
|
||||||
|
|
||||||
```c++
|
|
||||||
#include "sandboxed_api/sandbox2/sandbox2.h"
|
|
||||||
|
|
||||||
sandbox2::Sandbox2 s2(std::move(executor), std::move(policy));
|
|
||||||
auto result = s2.Run(); // Synchronous
|
|
||||||
LOG(INFO) << "Result of sandbox execution: " << result.ToString();
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also run it asynchronously, for instance to communicate with the
|
|
||||||
sandboxee. For examples see [crc4](examples.md#CRC4) and [sandbox
|
|
||||||
tool](examples.md#tool).
|
|
||||||
|
|
||||||
```c++
|
|
||||||
#include "sandboxed_api/sandbox2/sandbox2.h"
|
|
||||||
|
|
||||||
sandbox2::Sandbox2 s2(std::move(executor), std::move(policy));
|
|
||||||
if (s2.RunAsync()) {
|
|
||||||
... // Communicate with sandboxee, use s2.Kill() to kill it if needed
|
|
||||||
}
|
|
||||||
auto result = s2.AwaitResult();
|
|
||||||
LOG(INFO) << "Final execution status: " << result.ToString();
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Communicating with the sandboxee
|
|
||||||
|
|
||||||
The executor can communicate with the sandboxee with file descriptors.
|
|
||||||
|
|
||||||
Depending on your situation, that can be all that you need (e.g., to share a
|
|
||||||
file with the sandboxee or to read the sandboxee standard output).
|
|
||||||
|
|
||||||
If you need more communication logic, you can implement your own protocol or
|
|
||||||
reuse our convenient **comms** API able to send integers, strings, byte
|
|
||||||
buffers, protobufs or file descriptors. Bonus: in addition to C++, we also
|
|
||||||
provide a pure-C comms library, so it can be used easily when sandboxing C
|
|
||||||
third-party projects.
|
|
||||||
|
|
||||||
### a. Sharing file descriptors
|
|
||||||
|
|
||||||
Using the [IPC](../ipc.h) (*Inter-Process Communication*) API, you can either:
|
|
||||||
|
|
||||||
* use `MapFd()` to map file descriptors from the executor to the sandboxee, for
|
|
||||||
instance to share a file opened from the executor for use in the sandboxee,
|
|
||||||
as it is done in the [static](examples.md#static) example.
|
|
||||||
|
|
||||||
```c++
|
|
||||||
// The executor opened /proc/version and passes it to the sandboxee as stdin
|
|
||||||
executor->ipc()->MapFd(proc_version_fd, STDIN_FILENO);
|
|
||||||
```
|
|
||||||
or
|
|
||||||
|
|
||||||
* use `ReceiveFd()` to create a socketpair endpoint, for instance to read the
|
|
||||||
sandboxee standard output or standard error, as it is done in the
|
|
||||||
[sandbox tool](examples.md#tool) example.
|
|
||||||
|
|
||||||
```c++
|
|
||||||
// The executor receives a file descriptor of the sandboxee stdout
|
|
||||||
int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
|
|
||||||
```
|
|
||||||
|
|
||||||
### b. Using the comms API
|
|
||||||
|
|
||||||
Using the [comms](../comms.h) API, you can send integers, strings or byte
|
|
||||||
buffers. For an example see [crc4](examples.md#CRC4).
|
|
||||||
|
|
||||||
To use comms, first get it from the executor IPC:
|
|
||||||
|
|
||||||
```c++
|
|
||||||
auto* comms = executor->ipc()->GetComms();
|
|
||||||
```
|
|
||||||
|
|
||||||
To send data to the sandboxee, use one of the `Send*` family of functions.
|
|
||||||
For instance in the case of [crc4](examples.md#CRC4), the executor sends an
|
|
||||||
`unsigned char buf[size]` with `SendBytes(buf, size)`:
|
|
||||||
|
|
||||||
```c++
|
|
||||||
if (!(comms->SendBytes(static_cast<const uint8_t*>(buf), sz))) {
|
|
||||||
/* handle error */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
To receive data from the sandboxee, use one of the `Recv*` functions. For
|
|
||||||
instance in the case of [crc4](examples.md#CRC4), the executor receives the
|
|
||||||
checksum into an 32-bit unsigned integer:
|
|
||||||
|
|
||||||
```c++
|
|
||||||
uint32_t crc4;
|
|
||||||
if (!(comms->RecvUint32(&crc4))) {
|
|
||||||
/* handle error */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### c. Sharing data with buffers
|
|
||||||
|
|
||||||
In some situations, it can be useful to share data between executor and
|
|
||||||
sandboxee in order to share large amounts of data and to avoid expensive copies
|
|
||||||
that are sent back and forth. The [buffer API](../buffer.h) serves this use
|
|
||||||
case: the executor creates a `Buffer`, either by size and data to be passed, or
|
|
||||||
directly from a file descriptor, and passes it to the sandboxee using
|
|
||||||
`comms->SendFD()` in the executor and `comms->RecvFD()` in the sandboxee.
|
|
||||||
|
|
||||||
For example, to create a buffer in the executor, send its file descriptor to
|
|
||||||
the sandboxee, and afterwards see what the sandboxee did with it:
|
|
||||||
|
|
||||||
```c++
|
|
||||||
sandbox2::Buffer buffer;
|
|
||||||
buffer.Create(1ULL << 20); // 1 MiB
|
|
||||||
s2.RunAsync();
|
|
||||||
comms->SendFD(buffer.GetFD());
|
|
||||||
auto result = s2.AwaitResult();
|
|
||||||
uint8_t* buf = buffer.buffer(); // As modified by sandboxee
|
|
||||||
size_t len = buffer.size();
|
|
||||||
```
|
|
||||||
|
|
||||||
On the other side the sandboxee receives the buffer file descriptor, creates the
|
|
||||||
buffer object and can work with it:
|
|
||||||
|
|
||||||
```c++
|
|
||||||
int fd;
|
|
||||||
comms.RecvFD(&fd);
|
|
||||||
sandbox2::Buffer buffer;
|
|
||||||
buffer.Setup(fd);
|
|
||||||
uint8_t *buf = buffer.GetBuffer();
|
|
||||||
memset(buf, 'X', buffer.GetSize()); /* work with the buffer */
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. Exiting
|
|
||||||
|
|
||||||
If running the sandbox synchronously, then `Run` will only return when it's
|
|
||||||
finished:
|
|
||||||
|
|
||||||
```c++
|
|
||||||
auto result = s2.Run();
|
|
||||||
LOG(INFO) << "Final execution status: " << result.ToString();
|
|
||||||
```
|
|
||||||
|
|
||||||
If running asynchronously, you can decide at anytime to kill the sandboxee:
|
|
||||||
|
|
||||||
```c++
|
|
||||||
s2.Kill()
|
|
||||||
```
|
|
||||||
|
|
||||||
Or just wait for completion and the final execution status:
|
|
||||||
|
|
||||||
```c++
|
|
||||||
auto result = s2.AwaitResult();
|
|
||||||
LOG(INFO) << "Final execution status: " << result.ToString();
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. Test
|
|
||||||
|
|
||||||
Like regular code, your sandbox implementation should have tests. Sandbox tests
|
|
||||||
are not meant to test the program correctness, but instead to check whether the
|
|
||||||
sandboxed program can run without issues like sandbox violations. This also
|
|
||||||
makes sure that the policy is correct.
|
|
||||||
|
|
||||||
A sandboxed program is tested the same way it would run in production, with the
|
|
||||||
arguments and input files it would normally process.
|
|
||||||
|
|
||||||
It can be as simple as a shell test or C++ tests using sub processes. Check out
|
|
||||||
[the examples](examples.md) for inspiration.
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
Thanks for reading this far, we hope you liked our guide and now feel empowered
|
|
||||||
to create your own sandboxes to help keep your users safe.
|
|
||||||
|
|
||||||
Creating sandboxes and policies is a difficult task prone to subtle errors. To
|
|
||||||
remain on the safe side, have a security expert review your policy and code.
|
|
|
@ -1,57 +0,0 @@
|
||||||
# How it works
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The sandbox technology is organized around 2 processes:
|
|
||||||
|
|
||||||
* An **executor** sets up and runs the *monitor*:
|
|
||||||
* Also known as *parent*, *supervisor* or *monitor*
|
|
||||||
* By itself is not sandboxed
|
|
||||||
* Is regular C++ code using the Sandbox2 API
|
|
||||||
|
|
||||||
* The **sandboxee**, a child program running in the sandboxed environment:
|
|
||||||
* Also known as *child* or *sandboxed process*
|
|
||||||
* Receives its policy from the executor and applies it
|
|
||||||
* Can come in different shapes:
|
|
||||||
* Another binary, like in the [crc4](../examples/crc4/crc4sandbox.cc) and
|
|
||||||
[static](../examples/static/static_sandbox.cc) examples
|
|
||||||
* A third party binary for which you do not have the source
|
|
||||||
|
|
||||||
Purpose/goal:
|
|
||||||
|
|
||||||
* Restrict the sandboxee to a set of allowed syscalls and their arguments
|
|
||||||
* The tighter the policy, the better
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
A really tight policy could deny all except reads and writes on standard
|
|
||||||
input and output file descriptors. Inside this sandbox, a program could take
|
|
||||||
input, process it, and send the output back.
|
|
||||||
* The processing is not allowed to make any other syscall, or else it is killed
|
|
||||||
for policy violation.
|
|
||||||
* If the processing is compromised (code execution by a malicious user), it
|
|
||||||
cannot do anything bad other than producing bad output (that the executor and
|
|
||||||
others still need to handle correctly).
|
|
||||||
|
|
||||||
|
|
||||||
## Sandbox Policies
|
|
||||||
|
|
||||||
The sandbox relies on **seccomp-bpf** provided by the Linux kernel. **seccomp**
|
|
||||||
is a Linux kernel facility for sandboxing and **BPF** is a way to write syscall
|
|
||||||
filters (the very same used for network filters). Read more about
|
|
||||||
[seccomp-bpf on Wikipedia](https://en.wikipedia.org/wiki/Seccomp#seccomp-bpf).
|
|
||||||
|
|
||||||
In practice, you will generate your policy using our
|
|
||||||
[PolicyBuilder class](../policybuilder.h). If you need more complex rules, you
|
|
||||||
can specify raw BPF macros, like in the [crc4](../examples/crc4/crc4sandbox.cc)
|
|
||||||
example.
|
|
||||||
|
|
||||||
Filesystem accesses are restricted with the help of Linux
|
|
||||||
[user namespaces](http://man7.org/linux/man-pages/man7/user_namespaces.7.html).
|
|
||||||
User namespaces allow to drop the sandboxee into a custom chroot environment
|
|
||||||
without requiring root privileges.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
Read our [Getting started](getting-started.md) page to set up your first
|
|
||||||
sandbox.
|
|
Loading…
Reference in New Issue
Block a user