Guía de transacciones

Introducción

Cuando se usa una biblioteca de C/C++ sin zona de pruebas, el vinculador garantiza que todas las funciones necesarias estén disponibles después de la compilación y, por lo tanto, no hay necesidad de preocuparse si una llamada a la API puede fallar durante el tiempo de ejecución.

Sin embargo, cuando se usa una biblioteca con zona de pruebas, la ejecución de la biblioteca se aloja en un proceso independiente. Una falla en una llamada a la API requiere la comprobación de todo tipo de problemas relacionados con el paso de la llamada por la capa de RPC. A veces, los errores de la capa de RPC pueden no ser interesantes, por ejemplo, cuando se realiza un procesamiento masivo y la zona de pruebas se acaba de reiniciar.

Sin embargo, por los motivos mencionados anteriormente, es importante extender la comprobación de errores habitual del valor de retorno de la llamada a la API de zona de pruebas para incluir la verificación de si se mostró un error en la capa de RPC. Por eso, todos los prototipos de funciones de la biblioteca muestran ::sapi::StatusOr<T> en lugar de T. En caso de que falle la invocación de la función de biblioteca (p.ej., debido a un incumplimiento de la zona de pruebas), el valor de retorno contendrá detalles sobre el error que se produjo.

El manejo de los errores de la capa de RPC implicaría que cada llamada a una biblioteca de zona de pruebas es seguida de una verificación adicional de la capa de RPC de SAPI. Para lidiar con esas situaciones excepcionales, SAPI proporciona el módulo SAPI Transaction (transaction.h). Este módulo contiene la clase ::sapi::Transaction y garantiza que todas las llamadas a funciones a la biblioteca de la zona de pruebas se hayan completado sin problemas a nivel de RPC o que muestren un error relevante.

Transacción SAPI

El SAPI aísla el código de host de la biblioteca de la zona de pruebas y ofrece al emisor la capacidad de reiniciar o anular la solicitud de procesamiento de datos problemática. La transacción SAPI va un paso más allá y repite automáticamente los procesos con errores.

Las transacciones de SAPI se pueden usar de dos maneras diferentes: heredando directamente de ::sapi::Transaction o mediante punteros de función pasados a ::sapi::BasicTransaction.

Las transacciones de SAPI se definen anulando las siguientes tres funciones:

Métodos de transacción de SAPI
::sapi::Transaction::Init() Esto es similar a llamar a un método de inicialización de una biblioteca C/C++ típica. Se llama al método solo una vez durante cada transacción a la biblioteca de la zona de pruebas, a menos que se reinicie la transacción. En el caso de un reinicio, se vuelve a llamar al método, sin importar cuántos reinicios hayan ocurrido antes.
::sapi::Transaction::Main() Se llama al método para cada llamada a ::sapi::Transaction::Run() .
::sapi::Transaction::Finish() Es similar a llamar a un método de limpieza de una biblioteca C/C++ típica. El método se llama solo una vez durante la destrucción del objeto de transacción SAPI.

Uso normal de la biblioteca

En un proyecto sin bibliotecas de zona de pruebas, el patrón habitual cuando se trata de bibliotecas se ve de la siguiente manera:

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

Se inicializa la biblioteca, se usan las funciones exportadas de la biblioteca y, por último, se llama a una función end/close para limpiar el entorno.

Uso de bibliotecas en zona de pruebas

En un proyecto con bibliotecas de zona de pruebas, el código de Uso normal de la biblioteca se traduce en el siguiente fragmento de código cuando se usan transacciones con devoluciones de llamada:

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

La clase de transacción se asegura de reiniciar la biblioteca en caso de que se produzca un error durante la invocación de handle_data. Obtén más información sobre este tema en la siguiente sección.

Reinicios de transacción

Si una llamada a la API de la biblioteca de zona de pruebas genera un error durante la ejecución de los métodos de transacción de SAPI (consulta la tabla anterior), se reiniciará la transacción. kDefaultRetryCnt define la cantidad predeterminada de reinicios en transaction.h.

Estos son algunos ejemplos de errores generados que activarán un reinicio:

  • Se produjo un incumplimiento en la zona de pruebas
  • El proceso de la zona de pruebas falló
  • Una función de zona de pruebas mostró un código de error debido a un error de la biblioteca

El procedimiento de reinicio observa el flujo normal de Init() y Main() y, si las llamadas repetidas al método ::sapi::Transaction::Run() muestran errores, todo el método muestra un error a su llamador.

Manejo de errores de zona de pruebas o RPC

La interfaz de la biblioteca de zona de pruebas generada automáticamente intenta parecerse lo más posible al prototipo de la función de la biblioteca C/C++ original. Sin embargo, la biblioteca de zona de pruebas debe poder indicar cualquier error de zona de pruebas o RPC.

Para ello, se usan los tipos de datos que se muestran de ::sapi::StatusOr<T> (o ::sapi::Status para las funciones que muestran void), en lugar de mostrar directamente el valor que se muestra de las funciones de zona de pruebas.

SAPI también proporciona algunas macros convenientes para verificar un objeto de estado SAPI y reaccionar a él. Estas macros se definen en el archivo de encabezado status_macro.h.

El siguiente fragmento de código es un extracto del ejemplo de suma y muestra el uso del estado SAPI y las 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();
}

Reinicios de la zona de pruebas

Muchas bibliotecas de zonas de pruebas controlan las entradas sensibles del usuario. Si la biblioteca de la zona de pruebas se daña en algún momento y almacena datos entre ejecuciones, estos datos sensibles están en riesgo. Por ejemplo, si una versión de zona de pruebas de la biblioteca de Imagemagick comienza a enviar imágenes de la ejecución anterior.

Para evitar esta situación, la zona de pruebas no debe reutilizarse en varias ejecuciones. Para detener la reutilización de las zonas de pruebas, el código de host puede iniciar un reinicio del proceso de la biblioteca de la zona de pruebas mediante ::sapi::Sandbox::Restart() o ::sapi::Transaction::Restart() cuando se usan transacciones SAPI.

Un reinicio invalidará cualquier referencia al proceso de la biblioteca de zona de pruebas. Esto significa que los descriptores de archivos pasados o la memoria asignada ya no existirán.