Transactions Guide

Introduction

When using an unsandboxed C/C++ library, the linker ensures all necessary functions are available after compilation and thus there is no need to worry whether an API call may fail at runtime.

When using a Sandboxed Library however, the execution of the library lives in a separate process. A failure in an API call requires checking for all kinds of problems related to passing the call over the RPC layer. Sometimes, the RPC layer errors might not be of interest, for example when doing bulk processing and the sandbox just got restarted.

Nevertheless, for the reasons mentioned above, it is important to extend regular error checking of the sandboxed API call's return value to include checking if an error was returned on the RPC layer. This 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.

Handling of the RPC layer errors would mean that each call to a Sandboxed Library is followed by an additional check of the RPC layer of SAPI. In order to deal with those exceptional situations, SAPI provides the SAPI Transaction module (transaction.h). This module contains the ::sapi::Transaction class and makes sure that all function calls to the Sandboxed Library were completed without any RPC-level problems, or return a relevant error.

SAPI Transaction

SAPI isolates the Host Code from the Sandboxed Library and gives the caller the ability to restart or abort the problematic data processing request. SAPI Transaction goes one step further and automatically repeats failed processes.

SAPI Transactions can be used in two different ways: either directly inheriting from ::sapi::Transaction, or using function pointers passed to ::sapi::BasicTransaction.

SAPI Transactions are defined by overriding the following three functions:

SAPI Transaction Methods
::sapi::Transaction::Init() This is similar to calling an initialization method of a typical C/C++ library. The method is called only once during each transaction to the Sandboxed Library, unless the transaction is restarted. In the case of a restart, the method is called again, regardless of how many restarts have happened before.
::sapi::Transaction::Main() The method is called for each call to ::sapi::Transaction::Run() .
::sapi::Transaction::Finish() This is similar to calling a clean-up method of a typical C/C++ library. The method is called only once during SAPI Transaction object destruction.

Normal Library Use

In a project without sandboxed libraries, the usual pattern when dealing with libraries looks something like this:

LibInit();
while (data = NextDataToProcess()) {
  result += LibProcessData(data);
}
LibClose();

The library is initialized, then exported functions of the library are used, and finally an end/close function is called to clean-up the environment.

Sandboxed Library Use

In a project with sandboxed libraries, the code from Normal Library Use translates into the following code snippet when using transactions with callbacks:

// Overwrite the Init method, passed to BasicTransaction initialization
::absl::Status Init(::sapi::Sandbox* sandbox) {
  // Instantiate the SAPI Object
  LibraryAPI lib(sandbox);
  // Instantiate the Sandboxed Library
  SAPI_RETURN_IF_ERROR(lib.LibInit());
  return ::absl::OkStatus();
}

// Overwrite the Finish method, passed to BasicTransaction initialization
::absl::Status Finish(::sapi::Sandbox *sandbox) {
  // Instantiate the SAPI Object
  LibraryAPI lib(sandbox);
  // Clean-up sandboxed library instance
  SAPI_RETURN_IF_ERROR(lib.LibClose());
  return ::absl::OkStatus();
}

// Wrapper function to process data, passed to Run method call
::absl::Status HandleData(::sapi::Sandbox *sandbox, Data data_to_process,
                           Result *out) {
  // Instantiate the SAPI Object
  LibraryAPI lib(sandbox);
  // Call the sandboxed function LibProcessData
  SAPI_ASSIGN_OR_RETURN(*out, lib.LibProcessData(data_to_process));
  return ::absl::OkStatus();
}

void Handle() {
  // Use SAPI Transactions by passing function pointers to ::sapi::BasicTransaction
  ::sapi::BasicTransaction transaction(Init, Finish);
  while (data = NextDataToProcess()) {
    ::sandbox2::Result result;
    // call the ::sapi::Transaction::Run() method
    transaction.Run(HandleData, data, &result);
    // ...
  }
  // ...
}

The transaction class makes sure to reinitialize the library in case an error occurred during the handle_data invocation – more on this in the following section.

Transaction Restarts

If a sandboxed library API call raises an error during the execution of the SAPI Transaction methods (see table above), the transaction will be restarted. The default number of restarts is defined by kDefaultRetryCnt in transaction.h.

Examples of raised errors that will trigger a restart are:

  • A sandbox violation occurred
  • The sandboxed process crashed
  • A sandboxed function returned an error code due to a library error

The restart procedure observes the normal Init() and Main() flow, and if repeated calls to the ::sapi::Transaction::Run() method return errors, then the whole method returns an error to its caller

Sandbox or RPC Error Handling

The auto-generated Sandboxed Library interface attempts to be as close to the original C/C++ library function prototype as possible. However, the Sandboxed Library needs to be able to signal any sandbox or RPC errors.

This is achieved by making use of ::sapi::StatusOr<T> return types (or ::sapi::Status for functions returning void), instead of returning the sandboxed functions' return value directly.

SAPI further provides some convenient macros to check and react to a SAPI Status object. These macros are defined in the status_macro.h header file.

The following code snippet is an excerpt from the sum example and demonstrates the use of SAPI Status and the macros:

// Instead of void, use ::sapi::Status
::sapi::Status SumTransaction::Main() {
  // Instantiate the SAPI Object
  SumApi f(sandbox());

  // ::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()));
  // ...
  // Gets symbol address and prints its value
  int *ssaddr;
  SAPI_RETURN_IF_ERROR(sandbox()->Symbol(
      "sumsymbol", reinterpret_cast<void**>(&ssaddr)));
  ::sapi::v::Int sumsymbol;
  sumsymbol.SetRemote(ssaddr);
  SAPI_RETURN_IF_ERROR(sandbox()->TransferFromSandboxee(&sumsymbol));
  // ...
  return ::sapi::OkStatus();
}

Sandbox Restarts

Many sandboxed libraries handle sensitive user input. If the sandboxed library is corrupted at some point and stores data between runs, this sensitive data is at risk. For example, if a sandboxed version of the Imagemagick library starts sending out pictures of the previous run.

To avoid such a scenario, the sandbox should not be reused for multiple runs. To stop the reuse of sandboxes, the Host Code can initiate a restart of the sandboxed library process using ::sapi::Sandbox::Restart() or ::sapi::Transaction::Restart() when using SAPI Transactions.

A restart will invalidate any reference to the sandboxed library process. This means that passed file descriptors, or allocated memory will no longer exist.