Guia de transações

Introdução

Ao usar uma biblioteca C/C++ fora do sandbox, o vinculador garante que todas as funções necessárias estejam disponíveis após a compilação e, portanto, não há necessidade de se preocupar se uma chamada de API pode falhar no momento da execução.

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

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

O tratamento dos erros da camada RPC significa que cada chamada para uma biblioteca no modo 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 SAPI (transaction.h). Esse módulo contém a classe ::sapi::Transaction e garante que todas as chamadas de função para a biblioteca no modo sandbox sejam concluídas sem problemas no nível da RPC ou retornam um erro relevante.

Transação SAPI

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

As transações 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 SAPI são definidas pela substituição das três funções a seguir:

Métodos de transação 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 no modo sandbox, a menos que a transação seja reiniciada. No caso de uma reinicialização, o método é chamado novamente, independentemente de quantas reinicializações aconteceram antes.
::sapi::Transaction::Main() O método é chamado a cada chamada para ::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 da Transação SAPI.

Uso normal da biblioteca

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

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

A biblioteca é inicializada, as funções exportadas são usadas e, por fim, uma função "end/close" é chamada para limpar o ambiente.

Uso da Biblioteca no modo sandbox

Em um projeto com bibliotecas em sandbox, o código do Uso normal da biblioteca é convertido no 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 do 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 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 acionarão 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 observa o fluxo normal de Init() e Main() e, se chamadas repetidas ao método ::sapi::Transaction::Run() retornarem erros, o método inteiro retornará um erro ao autor da chamada.

Tratamento de erros de sandbox ou RPC

A interface da biblioteca no sandbox gerada automaticamente tenta ficar o mais próximo possível do protótipo original da função da biblioteca C/C++. No entanto, a biblioteca no modo sandbox precisa sinalizar qualquer erro de sandbox ou RPC.

Isso é possível usando os 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.

Além disso, o SAPI fornece algumas macros convenientes para verificar e reagir a um objeto de status do 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 de soma e demonstra o uso do status 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 de sandbox

Muitas bibliotecas em sandbox lidam com entradas confidenciais 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 imagens da execução anterior.

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

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