5. Communicating with the Sandboxee

By default, the executor can communicate with the Sandboxee through file descriptors. This might be all you need, for example if you just want to share a file with the Sandboxee, or read the Sandboxee's standard output.

However, you most likely have the need for more complex communication logic between the executor and Sandboxee. The comms API (see the comms.h header file) can be used to send integers, strings, byte buffers, protobufs, or file descriptors.

Sharing File Descriptors

Using the Inter-Process Communication API (see ipc.h), you can use MapFd() or ReceiveFd():

  • Use MapFd() to map file descriptors from the executor to the Sandboxee. This can be used to share a file opened from the executor for use in the Sandboxee. An example use can be seen in static.

    // The executor opened /proc/version and passes it to the sandboxee as stdin
    executor->ipc()->MapFd(proc_version_fd, STDIN_FILENO);
    
  • UseReceiveFd() to create a socketpair endpoint. This can be used to read the Sandboxee's standard output or standard errors. An example use can be seen in the tool.

    // The executor receives a file descriptor of the sandboxee stdout
    int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
    

Using the comms API

Sandbox2 provides a convenient comms API. This is a simple and easy way to share integers, strings, or byte buffers between the executor and Sandboxee. Below are some code snippets that you can find in the crc4 example.

To get started with the comms API, you first have to get the comms object from the Sandbox2 object:

sandbox2::Comms* comms = s2.comms();

Once the comms object is available, data can be sent to the Sandboxee using one of the Send* family of functions. You can find an example use of the comms API in the crc4 example. The code snippet below shows an excerpt from that example. The executor sends an unsigned char buf[size] with SendBytes(buf, size):

if (!(comms->SendBytes(static_cast<const uint8_t*>(buf), sz))) {
  /* handle error */
}

To receive data from the Sandboxee, use one of the Recv* functions. The code snippet below is an excerpt from the crc4 example. The executor receives the checksum in a 32-bit unsigned integer: uint32_t crc4;

if (!(comms->RecvUint32(&crc4))) {
  /* handle error */
}

Sharing Data with Buffers

Another data sharing functionality is to use the buffer API to share large amounts of data and to avoid expensive copies that are sent back and forth between the executor and Sandboxee.

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.

In the code snippet below, you can see the executor's side. The sandbox runs asynchronously and shares data via a buffer with the Sandboxee:

// start the sandbox asynchronously
s2.RunAsync();

// instantiate the comms object
sandbox2::Comms* comms = s2.comms();

// random buffer data we want to send
constexpr unsigned char buffer_data[] = /* random data */;
constexpr unsigned int buffer_dataLen = 34;

// create sandbox2 buffer
absl::StatusOr<std::unique_ptr<sandbox2::Buffer>> buffer =
     sandbox2::Buffer::CreateWithSize(1ULL << 20 /* 1Mib */);
std::unique_ptr<sandbox2::Buffer> buffer_ptr = std::move(buffer).value();

// point to the sandbox2 buffer and fill with data
uint8_t* buf = buffer_ptr>data();
memcpy(buf, buffer_data, buffer_data_len);

// send the data to the sandboxee
comms>SendFd(buffer_ptr>fd());

On the Sandboxee's side, you also have to create a buffer object and read the data from the file descriptor sent by the executor:

// establish the communication with the executor
int fd;
comms.RecvFD(&fd);

// create the buffer
absl::StatusOr<std::unique_ptr<sandbox2::Buffer>> buffer =
     sandbox2::Buffer::createFromFd(fd);

// get the data
auto buffer_ptr = std::move(buffer).value();
uint8_t* buf = buffer_ptr>data();

/* work with the buf object */