交易指南

简介

使用非沙盒化的 C/C++ 库时,链接器会确保在编译后所有必需的函数都可用,因此无需担心 API 调用是否会在运行时失败。

不过,使用沙盒库时,库的执行位于单独的进程中。如果 API 调用失败,则需要检查与通过 RPC 层传递调用相关的所有类型的问题。有时,RPC 层错误可能并不重要,例如在进行批量处理时,沙盒刚刚重启。

不过,出于上述原因,有必要扩展沙盒 API 调用返回值的常规错误检查,以包括检查 RPC 层是否返回了错误。因此,所有库函数原型都返回 ::sapi::StatusOr<T> 而不是 T。如果库函数调用失败(例如,由于违反了沙盒规则),返回值将包含有关所发生错误的详细信息。

处理 RPC 层错误意味着,每次调用沙盒库后,都会额外检查 SAPI 的 RPC 层。为了应对这些特殊情况,SAPI 提供了 SAPI 事务模块 (transaction.h)。此模块包含 ::sapi::Transaction 类,并确保对沙盒库的所有函数调用都已完成,且没有出现任何 RPC 级问题,或者返回相关错误。

SAPI 交易

SAPI 将宿主代码与沙盒库隔离开来,并让调用者能够重启或中止有问题的数据处理请求。SAPI 事务更进一步,可自动重复失败的流程。

SAPI 事务可以采用两种不同的方式使用:直接从 ::sapi::Transaction 继承,或使用传递给 ::sapi::BasicTransaction 的函数指针。

SAPI 交易通过替换以下三个函数来定义:

SAPI 交易方法
::sapi::Transaction::Init() 这类似于调用典型 C/C++ 库的初始化方法。 在每次与沙盒库的交易期间,该方法仅被调用一次,除非交易重新开始。 如果发生重启,无论之前发生了多少次重启,系统都会再次调用该方法。
::sapi::Transaction::Main() 每次调用 ::sapi::Transaction::Run() 时都会调用该方法。
::sapi::Transaction::Finish() 这类似于调用典型 C/C++ 库的清理方法。 该方法仅在 SAPI 事务对象销毁期间调用一次。

正常使用图书馆

在没有沙盒化库的项目中,处理库时的常见模式如下所示:

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

该库先进行初始化,然后使用该库的导出函数,最后调用结束/关闭函数来清理环境。

沙盒化库使用

在包含沙盒库的项目中,常规库使用中的代码在使用带回调的事务时会转换为以下代码段:

// 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);
    // ...
  }
  // ...
}

事务类可确保在 handle_data 调用期间发生错误时重新初始化库 - 有关此方面的更多信息,请参阅下一部分。

交易重启

如果沙盒化库 API 调用在执行 SAPI 交易方法(见上表)期间引发错误,交易将重新启动。默认的重启次数由 transaction.h 中的 kDefaultRetryCnt 定义。

会触发重新启动的已引发错误示例包括:

  • 发生了沙盒违规行为
  • 沙盒进程崩溃
  • 沙盒函数因库错误而返回了错误代码

重新启动程序会观察正常的 Init()Main() 流,如果对 ::sapi::Transaction::Run() 方法的重复调用返回错误,则整个方法会向其调用方返回错误

沙盒或 RPC 错误处理

自动生成的沙盒库接口会尽可能接近原始 C/C++ 库函数原型。不过,沙盒库需要能够发出任何沙盒或 RPC 错误信号。

这是通过使用 ::sapi::StatusOr<T> 返回类型(或对于返回 void 的函数,使用 ::sapi::Status)来实现的,而不是直接返回沙盒函数的返回值。

SAPI 进一步提供了一些便捷的宏,用于检查和响应 SAPI 状态对象。这些宏在 status_macro.h 标头文件中定义。

以下代码段摘自 sum 示例,展示了 SAPI 状态和宏的用法:

// 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();
}

沙盒重启

许多沙盒化库会处理敏感的用户输入。如果沙盒库在某个时间点损坏并存储了运行之间的数据,则这些敏感数据会面临风险。例如,如果沙盒版 Imagemagick 库开始发送之前运行的图片。

为避免出现这种情况,不应将沙盒重复用于多次运行。为了停止重用沙盒,宿主代码在使用 SAPI 事务时可以使用 ::sapi::Sandbox::Restart()::sapi::Transaction::Restart() 启动沙盒库进程的重启。

重新启动会使对沙盒库进程的任何引用失效。这意味着,传递的文件描述符或分配的内存将不再存在。