거래 가이드

소개

샌드박스 처리되지 않은 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에 전달된 함수 포인터를 사용하는 두 가지 방식으로 사용할 수 있습니다.

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();

라이브러리가 초기화되면 라이브러리의 내보낸 함수가 사용되고 마지막으로 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 호출 중에 오류가 발생할 경우 라이브러리를 다시 초기화합니다. 이에 관한 자세한 내용은 다음 섹션에서 설명합니다.

트랜잭션 재시작

SAPI 트랜잭션 메서드 (위 표 참고)를 실행하는 동안 샌드박스 처리된 라이브러리 API 호출에서 오류가 발생하면 트랜잭션이 다시 시작됩니다. 기본 재시작 횟수는 transaction.hkDefaultRetryCnt에 의해 정의됩니다.

재시작을 트리거하는 발생한 오류의 예는 다음과 같습니다.

  • 샌드박스 위반이 발생했습니다.
  • 샌드박스 처리된 프로세스가 비정상 종료됨
  • 샌드박스 처리된 함수에서 라이브러리 오류로 인해 오류 코드를 반환함

다시 시작 절차는 일반 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()를 사용하여 샌드박스 처리된 라이브러리 프로세스를 다시 시작할 수 있습니다.

다시 시작하면 샌드박스 처리된 라이브러리 프로세스에 대한 모든 참조가 무효화됩니다. 즉, 전달된 파일 설명자 또는 할당된 메모리가 더 이상 존재하지 않습니다.