Przewodnik po transakcjach

Wstęp

W przypadku biblioteki C/C++ poza piaskownicą linker zapewnia, że wszystkie niezbędne funkcje są dostępne po kompilacji, dzięki czemu nie trzeba się martwić, czy wywołanie interfejsu API może zakończyć się niepowodzeniem w czasie działania.

Jeśli jednak używasz biblioteki w trybie piaskownicy, jej uruchomienie odbywa się w osobnym procesie. Niepowodzenie wywołania interfejsu API wymaga sprawdzenia wszelkich problemów związanych z przekazywaniem wywołania przez warstwę RPC. Czasami błędy warstwy RPC mogą nie być interesujące, np. podczas przetwarzania zbiorczego, gdy piaskownica właśnie została ponownie uruchomiona.

Z powodów wymienionych powyżej warto jednak rozszerzyć regularne sprawdzanie wartości zwracanego wywołania interfejsu API w trybie piaskownicy, tak aby obejmowało sprawdzenie, czy w warstwie RPC wystąpił błąd. Dlatego wszystkie prototypy funkcji biblioteki zwracają wartość ::sapi::StatusOr<T> zamiast T. Jeśli nie uda się wywołać funkcji biblioteki (np. z powodu naruszenia zasad piaskownicy), zwracana wartość będzie zawierać szczegóły błędu.

Obsługa błędów warstwy RPC oznaczałaby, że po każdym wywołaniu biblioteki w trybie piaskownicy następuje dodatkowa kontrola warstwy RPC interfejsu SAPI. Na potrzeby takich wyjątkowych sytuacji interfejs SAPI udostępnia moduł transakcji SAPI (transaction.h). Ten moduł zawiera klasę ::sapi::Transaction i pilnuje, by wszystkie wywołania funkcji biblioteki piaskownicy w trybie piaskownicy zostały ukończone bez problemów na poziomie RPC lub zwracały odpowiedni błąd.

Transakcja SAPI

Interfejs SAPI izoluje kod hosta od biblioteki w trybie piaskownicy i daje elementowi wywołującemu możliwość ponownego uruchomienia lub przerwania problematycznego żądania przetwarzania danych. Transakcja SAPI idzie o krok dalej i automatycznie powtarza nieudane procesy.

Transakcjach SAPI może być używana na 2 sposoby: przez bezpośrednie dziedziczenie z elementu ::sapi::Transaction lub za pomocą wskaźników funkcji przekazanych do ::sapi::BasicTransaction.

Transakcje SAPI definiuje się przez zastąpienie tych 3 funkcji:

Metody transakcji SAPI
::sapi::Transaction::Init() Przypomina to wywoływanie metody inicjowania w typowej bibliotece C/C++. Metoda jest wywoływana tylko raz podczas każdej transakcji w Bibliotece w trybie piaskownicy, chyba że transakcja zostanie ponownie uruchomiona. W przypadku ponownego uruchomienia metoda jest wywoływana ponownie niezależnie od tego, ile ponownych uruchomień miało miejsce wcześniej.
::sapi::Transaction::Main() Metoda jest wywoływana przy każdym wywołaniu funkcji ::sapi::Transaction::Run() .
::sapi::Transaction::Finish() Przypomina to wywołanie metody czyszczenia w typowej bibliotece C/C++. Podczas niszczenia obiektu transakcji SAPI metoda jest wywoływana tylko raz.

Zwykłe korzystanie z biblioteki

W projekcie bez bibliotek w trybie piaskownicy zwykły wzorzec obsługi bibliotek wygląda mniej więcej tak:

LibInit();
while (data = NextDataToProcess()) {
  result += LibProcessData(data);
}
LibClose();

Biblioteka jest inicjowana, następnie używane są wyeksportowane funkcje biblioteki, a na koniec wywoływana jest funkcja end/close w celu oczyszczenia środowiska.

Korzystanie z biblioteki w trybie piaskownicy

Podczas korzystania z transakcji z wywołaniami zwrotnymi w projekcie zawierającym biblioteki w piaskownicy kod z normalnego użycia biblioteki jest zamieniany na ten fragment kodu:

// 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);
    // ...
  }
  // ...
}

Klasa transakcji dba o ponowne zainicjowanie biblioteki, jeśli podczas wywołania handle_data wystąpił błąd. Więcej informacji na ten temat znajdziesz w następnej sekcji.

Ponowne uruchomienie transakcji

Jeśli wywołanie interfejsu API biblioteki w trybie piaskownicy zwróci błąd podczas wykonywania metod transakcji SAPI (patrz tabela powyżej), transakcja zostanie uruchomiona ponownie. Domyślna liczba ponownych uruchomień jest określana przez kDefaultRetryCnt w tagu transaction.h.

Przykłady zgłoszonych błędów, które spowodują ponowne uruchomienie:

  • Wystąpiło naruszenie zasad piaskownicy
  • Proces w trybie piaskownicy uległ awarii
  • Funkcja w trybie piaskownicy zwróciła kod błędu z powodu błędu biblioteki

Procedura ponownego uruchamiania obserwuje normalny przepływ Init() i Main(). Jeśli wielokrotne wywołania metody ::sapi::Transaction::Run() zwracają błędy, cała metoda zwraca błąd do elementu wywołującego.

Obsługa błędów piaskownicy lub RPC

Automatycznie generowany interfejs biblioteki w trybie piaskownicy jest jak najbardziej zbliżony do pierwotnego prototypu funkcji biblioteki C/C++. Biblioteka w trybie piaskownicy musi jednak mieć możliwość sygnalizowania błędów piaskownicy lub RPC.

Można to osiągnąć przez stosowanie zwracanych typów ::sapi::StatusOr<T> (lub ::sapi::Status w przypadku funkcji zwracających void), zamiast bezpośredniego zwracania wartości zwracanej przez funkcje piaskownicy.

Interfejs SAPI udostępnia też przydatne makra, które umożliwiają sprawdzanie obiektu SAPI Status i reagowanie na nie. Te makra są zdefiniowane w pliku nagłówka status_macro.h.

Ten fragment kodu stanowi fragment przykładu z sumą i przedstawia użycie makr stanu SAPI i makr:

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

Ponowne uruchomienia piaskownicy

Wiele bibliotek w trybie piaskownicy obsługuje poufne dane wejściowe użytkownika. Jeśli biblioteka w trybie piaskownicy zostanie w pewnym momencie uszkodzona i będzie przechowywać dane pomiędzy uruchomieniami, te dane wrażliwe są zagrożone. Dzieje się tak na przykład wtedy, gdy z biblioteki Imagemagick w piaskownicy zacznie wysyłać zdjęcia z poprzedniego uruchomienia.

Aby uniknąć takiej sytuacji, nie należy używać piaskownicy do wielu uruchomień. Aby zatrzymać ponowne używanie piaskownicy, kod hosta może zainicjować ponowne uruchomienie procesu biblioteki w piaskownicy za pomocą polecenia ::sapi::Sandbox::Restart() lub ::sapi::Transaction::Restart() podczas korzystania z transakcji SAPI.

Ponowne uruchomienie unieważni każde odwołanie do procesu biblioteki w trybie piaskownicy. Oznacza to, że przekazane deskryptory plików lub przydzielona pamięć przestaną istnieć.