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.