交易指南

簡介

使用無沙箱防護的 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 Transaction 物件時,系統只會呼叫該方法一次。

正常使用資料庫

在沒有沙箱程式庫的專案中,處理程式庫的一般模式如下:

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 標頭檔案中定義。

以下程式碼片段摘錄自加總範例,示範如何使用 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() 重新啟動沙箱程式庫程序。

重新啟動後,任何對沙箱程式庫程序的參照都會失效。這表示傳遞的檔案描述元或分配的記憶體將不再存在。