1. Choose a Sandbox Executor Method

Sandboxing starts with an executor (see Sandbox Executor), which is responsible for running the Sandboxee. The executor.h header file contains the API needed for this purpose. The API is very flexible and lets you choose what works best for your use case. The following sections describe the 3 different methods from which you can choose.

Method 1: Stand-alone – Execute a binary with sandboxing already enabled

This is the simplest way to use sandboxing and is the recommended method when you want to sandbox an entire binary which you have no source code for. It is also the safest way to use sandboxing, as there is no unsandboxed initialization that could have adverse effects.

In the following code snippet, we define the path of the binary to be sandboxed and the arguments we have to pass to an execve syscall. As you can see in the executor.h header file, we don't specify a value for envp and therefore copy the environment from the parent process. Remember, the first argument is always the name of the program to be executed, and our snippet doesn't define any other argument.

Examples of this executor method are: static and tool.

#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);

Method 2: Sandbox2 Forkserver – Tell the executor when to be sandboxed

This method offers the flexibility of being unsandboxed during initialization, and then choosing when to enter sandboxing by calling ::sandbox2::Client::SandboxMeHere(). It requires you to be able to define in code when you want to start sandboxing, and it has to be single-threaded (read why in the FAQ).

In the following code snippet, we use the same code as outlined in Method 1 above. However, in order to allow the program to execute in an unsandboxed manner during initialization, we call set_enable_sandbox_before_exec(false).

#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);

As the executor now has a disabled sandbox until it is notified by the Sandboxee, we have to create a ::sandbox2::Client instance, set up communication between the executor and Sandboxee, and then notify the executor that our initialization is finished and we want to start sandboxing now by calling sandbox2_client.SandboxMeHere().

// main() of sandboxee
int main(int argc, char** argv) {
  gflags::ParseCommandLineFlags(&argc, &argv, false);

  // Set-up the sandbox2::Client object, using a file descriptor (1023).
  sandbox2::Comms comms(sandbox2::Comms::kSandbox2ClientCommsFD);
  sandbox2::Client sandbox2_client(&comms);
  // Enable sandboxing from here.
  sandbox2_client.SandboxMeHere();
  …

An example of this executor method is crc4, where crc4bin.cc is the Sandboxee and notifies the executor (crc4sandbox.cc) when it should enter the sandbox.

Method 3: Custom Forkserver – 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 a specific moment of your binary's lifecycle, make it available to the executor.

The executor will send a 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().

#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());

Keep in mind that this mode is quite complicated and applicable only in a few specific cases; for example, when you have tight memory requirements. You will benefit from COW but have the downside that there is no real ASLR. Another typcial usage example would be when the Sandboxee has a long, CPU-intensive initialization that can be run before the untrusted data is processed.

For an example of this executor method, see custom_fork.