交易指南

简介

在使用未经过沙盒屏蔽的 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();

先初始化库,然后使用库的导出函数,最后调用 end/close 函数来清理环境。

沙盒化库的使用

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

// 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() 启动沙盒化库进程的重启。

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