简介
使用非沙盒化的 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()
启动沙盒库进程的重启。
重新启动会使对沙盒库进程的任何引用失效。这意味着,传递的文件描述符或分配的内存将不再存在。