Guia de transações

Introdução

Ao usar uma biblioteca C/C++ sem sandbox, o vinculador garante que todas as funções necessárias estejam disponíveis após a compilação. Assim, não é preciso se preocupar se uma chamada de API pode falhar durante a execução.

No entanto, ao usar uma biblioteca em sandbox, a execução dela fica em um processo separado. Uma falha em uma chamada de API exige a verificação de todos os tipos de problemas relacionados à transmissão da chamada pela camada RPC. Às vezes, os erros da camada RPC podem não ser interessantes, por exemplo, ao fazer o processamento em lote e o sandbox acabou de ser reiniciado.

No entanto, pelos motivos mencionados acima, é importante estender a verificação regular de erros do valor de retorno da chamada de API em sandbox para incluir a verificação se um erro foi retornado na camada RPC. Por isso, todos os protótipos de função da biblioteca retornam ::sapi::StatusOr<T> em vez de T. Se a invocação da função de biblioteca falhar (por exemplo, devido a uma violação da sandbox), o valor de retorno vai conter detalhes sobre o erro ocorrido.

O processamento dos erros da camada RPC significa que cada chamada para uma biblioteca em sandbox é seguida por uma verificação adicional da camada RPC da SAPI. Para lidar com essas situações excepcionais, a SAPI fornece o módulo de transação da SAPI (transaction.h). Esse módulo contém a classe ::sapi::Transaction e garante que todas as chamadas de função para a biblioteca em sandbox foram concluídas sem problemas no nível de RPC ou retornam um erro relevante.

Transação da SAPI

A SAPI isola o código do host da biblioteca em sandbox e permite que o autor da chamada reinicie ou aborte a solicitação de processamento de dados problemática. A transação SAPI vai um passo além e repete automaticamente os processos com falha.

As transações da SAPI podem ser usadas de duas maneiras diferentes: herdando diretamente de ::sapi::Transaction ou usando ponteiros de função transmitidos para ::sapi::BasicTransaction.

As transações da SAPI são definidas substituindo estas três funções:

Métodos de transação da SAPI
::sapi::Transaction::Init() Isso é semelhante a chamar um método de inicialização de uma biblioteca C/C++ típica. O método é chamado apenas uma vez durante cada transação para a biblioteca em sandbox, a menos que a transação seja reiniciada. Em caso de reinicialização, o método é chamado novamente, não importa quantas reinicializações ocorreram antes.
::sapi::Transaction::Main() O método é chamado para cada chamada de ::sapi::Transaction::Run() .
::sapi::Transaction::Finish() Isso é semelhante a chamar um método de limpeza de uma biblioteca C/C++ típica. O método é chamado apenas uma vez durante a destruição do objeto de transação da SAPI.

Uso normal da biblioteca

Em um projeto sem bibliotecas em sandbox, o padrão usual ao lidar com bibliotecas é semelhante a este:

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

A biblioteca é inicializada, as funções exportadas dela são usadas e, por fim, uma função de encerramento/fechamento é chamada para limpar o ambiente.

Uso de biblioteca em sandbox

Em um projeto com bibliotecas em sandbox, o código de Uso normal da biblioteca é traduzido para o seguinte snippet de código ao usar transações com callbacks:

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

A classe de transação garante a reinicialização da biblioteca caso ocorra um erro durante a invocação de handle_data. Saiba mais sobre isso na próxima seção.

Reinicializações de transações

Se uma chamada de API de biblioteca em sandbox gerar um erro durante a execução dos métodos de transação da SAPI (consulte a tabela acima), a transação será reiniciada. O número padrão de reinicializações é definido por kDefaultRetryCnt em transaction.h.

Exemplos de erros gerados que vão acionar uma reinicialização:

  • Ocorreu uma violação do sandbox
  • O processo em sandbox falhou
  • Uma função em sandbox retornou um código de erro devido a um erro de biblioteca

O procedimento de reinicialização segue o fluxo normal de Init() e Main(). Se chamadas repetidas ao método ::sapi::Transaction::Run() retornarem erros, então o método inteiro vai retornar um erro para o chamador.

Tratamento de erros de sandbox ou RPC

A interface da biblioteca em sandbox gerada automaticamente tenta ser o mais próxima possível do protótipo da função da biblioteca C/C++ original. No entanto, a biblioteca em sandbox precisa ser capaz de sinalizar erros de sandbox ou RPC.

Isso é feito usando tipos de retorno ::sapi::StatusOr<T> (ou ::sapi::Status para funções que retornam void), em vez de retornar o valor de retorno das funções em sandbox diretamente.

A SAPI também oferece algumas macros convenientes para verificar e reagir a um objeto de status da SAPI. Essas macros são definidas no arquivo de cabeçalho status_macro.h.

O snippet de código a seguir é um trecho do exemplo sum e demonstra o uso do status da SAPI e das macros:

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

Reinicializações do sandbox

Muitas bibliotecas em sandbox processam entradas sensíveis do usuário. Se a biblioteca em sandbox for corrompida em algum momento e armazenar dados entre execuções, esses dados sensíveis estarão em risco. Por exemplo, se uma versão em sandbox da biblioteca Imagemagick começar a enviar fotos da execução anterior.

Para evitar essa situação, a sandbox não pode ser reutilizada para várias execuções. Para interromper a reutilização de sandboxes, o código do host pode iniciar uma reinicialização do processo da biblioteca em sandbox usando ::sapi::Sandbox::Restart() ou ::sapi::Transaction::Restart() ao usar transações SAPI.

Uma reinicialização vai invalidar qualquer referência ao processo da biblioteca em sandbox. Isso significa que os descritores de arquivo transmitidos ou a memória alocada não vão mais existir.