簡介
使用未經過沙箱處理的 C/C++ 程式庫時,連結器會確保編譯後提供所有必要函式,因此不必擔心 API 呼叫可能會在執行階段失敗。
不過,使用沙箱化程式庫時,程式庫的執行作業會位於獨立程序中。如果 API 呼叫失敗,就必須檢查與透過 RPC 層傳遞呼叫相關的所有問題。有時 RPC 層錯誤可能不重要,例如執行大量處理作業時,沙箱剛重新啟動。
不過,基於上述原因,請務必擴大對沙箱化 API 呼叫傳回值的定期錯誤檢查,包括檢查 RPC 層是否傳回錯誤。因此,所有程式庫函式原型都會傳回 ::sapi::StatusOr<T>
,而非 T。如果程式庫函式呼叫失敗 (例如因違反沙箱規定),回傳值會包含發生錯誤的詳細資料。
處理 RPC 層錯誤表示每次呼叫 SandboxedLibrary 後,都會額外檢查 SAPI 的 RPC 層。為處理這些例外狀況,SAPI 提供 SAPI 交易模組 (transaction.h)。這個模組包含 ::sapi::Transaction
類別,並確保對 Sandboxed Library 的所有函式呼叫都已完成,且沒有任何 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()
啟動沙箱化程式庫程序。
重新啟動會使任何對沙箱程式庫程序的參照失效。這表示傳遞的檔案描述元或分配的記憶體將不復存在。