取引ガイド

はじめに

サンドボックス化されていない C/C++ ライブラリを使用する場合、リンカーはコンパイル後に必要なすべての関数が利用できるため、API 呼び出しが実行時に失敗するかどうかを心配する必要はありません。

ただし、サンドボックス化されたライブラリを使用する場合、ライブラリの実行は別のプロセスで行われます。API 呼び出しの失敗では、RPC レイヤでの呼び出しを渡すことに関連するあらゆる種類の問題をチェックする必要があります。一括処理を実行してサンドボックスが再起動した直後など、RPC レイヤのエラーは重要でないことがあります。

それでも、前述の理由から、サンドボックス化された API 呼び出しの戻り値の定期的なエラーチェックを拡張して、RPC レイヤでエラーが返されたかどうかのチェックを含めることが重要です。このため、すべてのライブラリ関数のプロトタイプが T ではなく ::sapi::StatusOr<T> を返します。ライブラリ関数の呼び出しが失敗した場合(サンドボックス違反など)、戻り値には発生したエラーの詳細が含まれます。

RPC レイヤのエラーを処理するには、サンドボックス化されたライブラリを呼び出すたびに、SAPI の RPC レイヤの追加のチェックが行われます。このような例外的な状況に対処するために、SAPI には SAPI トランザクション モジュール(transaction.h)が用意されています。このモジュールには ::sapi::Transaction クラスが含まれており、サンドボックス ライブラリへのすべての関数呼び出しが RPC レベルの問題なしで完了していること、または関連するエラーを返していることを確認します。

SAPI トランザクション

SAPI は、サンドボックス化されたライブラリからホストコードを分離し、呼び出し元は問題のあるデータ処理リクエストを再開または中止できるようにします。SAPI トランザクションはさらに一歩進んで、失敗したプロセスを自動的に繰り返します。

SAPI トランザクションには、::sapi::Transaction から直接継承する方法と、::sapi::BasicTransaction に渡された関数ポインタを使用する方法の 2 つがあります。

SAPI トランザクションは、次の 3 つの関数をオーバーライドすることで定義されます。

SAPI トランザクション メソッド
::sapi::Transaction::Init() これは、一般的な C/C++ ライブラリの初期化メソッドを呼び出す場合と同様です。 このメソッドは、トランザクションを再開しない限り、サンドボックス化されたライブラリに対する各トランザクション中に 1 回だけ呼び出されます。 再起動の場合は、以前に再起動された回数に関係なく、メソッドが再度呼び出されます。
::sapi::Transaction::Main() このメソッドは、::sapi::Transaction::Run() を呼び出すたびに呼び出されます。
::sapi::Transaction::Finish() これは、一般的な C/C++ ライブラリのクリーンアップ メソッドを呼び出す場合と同様です。このメソッドは、SAPI トランザクション オブジェクトの破棄中に 1 回だけ呼び出されます。

通常ライブラリの使用

サンドボックス化されたライブラリのないプロジェクトでは、ライブラリを扱う通常のパターンは次のようになります。

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.hkDefaultRetryCnt で定義されます。

再起動を引き起こすエラーの例は次のとおりです。

  • サンドボックス違反が発生しました
  • サンドボックス化されたプロセスがクラッシュしました
  • サンドボックス化された関数がライブラリ エラーによりエラーコードを返しました

再開プロシージャは通常の Init()Main() のフローを監視します。::sapi::Transaction::Run() メソッドの繰り返し呼び出しがエラーを返す場合は、メソッド全体が呼び出し元にエラーを返します。

サンドボックスまたは RPC エラー処理

自動生成されるサンドボックス ライブラリ インターフェースは、元の C/C++ ライブラリ関数のプロトタイプにできるだけ近いものにしようとします。ただし、サンドボックス化されたライブラリは、サンドボックスまたは RPC エラーを通知できる必要があります。

これを実現するには、サンドボックス化された関数の戻り値を直接返すのではなく、::sapi::StatusOr<T> の戻り値の型(または void を返す関数の場合は ::sapi::Status)を使用します。

SAPI には、SAPI Status オブジェクトを確認して対応するための便利なマクロもあります。これらのマクロは、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() を使用して、サンドボックス化されたライブラリ プロセスの再起動を開始できます。

再起動すると、サンドボックス化されたライブラリ プロセスへの参照がすべて無効になります。つまり、渡されたファイル記述子や割り当て済みメモリはなくなります。