簡介
使用無沙箱防護的 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()
重新啟動沙箱程式庫程序。
重新啟動後,任何對沙箱程式庫程序的參照都會失效。這表示傳遞的檔案描述元或分配的記憶體將不再存在。