Guía para desarrolladores de C++ sobre los canales laterales de ejecución especulativa

Este artículo contiene instrucciones para ayudar a que los desarrolladores identifiquen y mitiguen las vulnerabilidades de hardware del canal lateral de ejecución especulativa en el software de C++. Estas vulnerabilidades pueden revelar información confidencial mediante límites de confianza y pueden afectar al software que se ejecuta en procesadores que admiten la ejecución especulativa y desordenada de instrucciones. Esta clase de vulnerabilidades se describió por primera vez en enero de 2018 y se pueden encontrar instrucciones y antecedentes adicionales en Aviso de seguridad de Microsoft.

Las instrucciones que proporciona este artículo están relacionadas con las clases de vulnerabilidades que representa lo siguiente:

  1. CVE-2017-5753, también conocido como variante 1 de Spectre. Esta clase de vulnerabilidad de hardware está relacionada con los canales laterales que pueden surgir debido a la ejecución especulativa que se produce como resultado de un error de una rama condicional. El compilador de Microsoft C++ en Visual Studio 2017 (a partir de la versión 15.5.5) incluye compatibilidad con el modificador /Qspectre que proporciona una mitigación en tiempo de compilación para un conjunto limitado de patrones de codificación potencialmente vulnerables relacionados con CVE-2017-5753. El modificador /Qspectre también está disponible en Visual Studio 2015 Update 3 mediante KB 4338871. La documentación de la marca /Qspectre proporciona más información sobre sus efectos y uso.

  2. CVE-2018-3639, también conocido como omisión de almacén especulativo (SSB). Esta clase de vulnerabilidad de hardware está relacionada con los canales laterales que pueden surgir debido a la ejecución especulativa de una carga por delante de un almacén dependiente como resultado de un error de acceso a la memoria.

Puede encontrar una introducción accesible a las vulnerabilidades de canal lateral de ejecución especulativa en la presentación titulada El caso de Spectre y Meltdown por uno de los equipos de investigación que detectaron estos problemas.

¿Qué son las vulnerabilidades de hardware del canal lateral de ejecución especulativa?

Las CPU modernas proporcionan mayores grados de rendimiento utilizando la ejecución especulativa y desordenada de instrucciones. Por ejemplo, esto se logra a menudo prediciendo el destino de las ramas (condicional e indirecta), lo que permite que la CPU comience a ejecutar instrucciones de forma especulativa en el destino de la rama prevista, lo que evita una detención hasta que se resuelva el destino de la rama real. En caso de que la CPU descubra más adelante que se ha producido un error de desuso, se descarta todo el estado de la máquina que se ha calculado de forma especulativa. Esto garantiza que no haya efectos arquitectónicamente visibles de la especulación errónea.

Aunque la ejecución especulativa no afecta al estado arquitectónicamente visible, puede dejar seguimientos residuales en un estado no arquitectónico, como las distintas memorias caché que usa la CPU. Son estos seguimientos residuales de la ejecución especulativa los que pueden dar lugar a vulnerabilidades de canal lateral. Para comprender mejor esto, considere el siguiente fragmento de código, que proporciona un ejemplo de CVE-2017-5753 (omisión de comprobación de límites):

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

En este ejemplo, ReadByte proporciona un búfer, un tamaño de búfer y un índice en ese búfer. El parámetro de índice, tal y como especifica untrusted_index, lo proporciona un contexto con menos privilegios, como un proceso no administrativo. Si untrusted_index es menor que buffer_size, el carácter de ese índice se lee desde buffer y se usa para indexar en una región compartida de memoria a la que shared_buffer hace referencia.

Desde una punto de vista arquitectónico, esta secuencia de código es perfectamente segura, ya que se garantiza que untrusted_index siempre será menor que buffer_size. Pero en presencia de ejecución especulativa, es posible que la CPU produzca un error en la rama condicional y ejecute el cuerpo de la instrucción if incluso cuando untrusted_index sea mayor o igual que buffer_size. Como consecuencia de esto, la CPU puede leer de forma especulativa un byte desde más allá de los límites de buffer (que podría ser un secreto) y, después, podría usar ese valor de byte para calcular la dirección de una carga posterior mediante shared_buffer.

Aunque la CPU finalmente detectará este error, es posible que queden efectos secundarios residuales en la memoria caché de CPU que revelen información sobre el valor de byte que se leyó fuera de los límites de buffer. Estos efectos secundarios los puede detectar un contexto con menos privilegios que se ejecuta en el sistema sondeando la rapidez con la que se accede a cada línea de caché de shared_buffer. Los pasos que se pueden realizar para realizar esto son los siguientes:

  1. Invocar ReadByte varias veces con un valor de untrusted_index menor que buffer_size. El contexto de ataque puede hacer que el contexto de la víctima invoque ReadByte (por ejemplo, mediante RPC), de modo que el predictor de rama esté entrenado para que no se tome como si untrusted_index fuera menor que buffer_size.

  2. Vaciar todas las líneas de caché en shared_buffer. El contexto de ataque debe vaciar todas las líneas de caché de la región compartida de memoria a la que shared_buffer hace referencia. Dado que la región de memoria está compartida, este paso es sencillo y se puede lograr con intrínsecos como _mm_clflush.

  3. Invocar ReadByte con un valor de untrusted_index mayor que buffer_size. El contexto de ataque hace que el contexto de la víctima invoque ReadByte de forma que predice incorrectamente que la rama no se tomará. Esto hace que el procesador ejecute de forma especulativa el cuerpo del bloque if con un valor de untrusted_index mayor que buffer_size, lo que lleva a una lectura fuera de los límites de buffer. Por lo tanto, shared_buffer se indexa mediante un valor potencialmente secreto que se leyó fuera de los límites, lo que hace que la CPU cargue la línea de caché correspondiente.

  4. Leer cada línea de caché en shared_buffer para ver a cuál se accede más rápidamente. El contexto de ataque puede leer cada línea de caché en shared_buffer y detectar la línea de caché que se carga considerablemente más rápido que las demás. Esta es la línea de caché que probablemente se haya incorporado en el paso 3. Dado que hay una relación de 1:1 entre el valor de bytes y la línea de caché en este ejemplo, esto permite al atacante deducir el valor real del byte que se leyó fuera de los límites.

Los pasos anteriores proporcionan un ejemplo del uso de una técnica conocida como FLUSH+RELOAD junto con la explotación de una instancia de CVE-2017-5753.

¿Qué escenarios de software se pueden ver afectados?

El desarrollo de software seguro mediante un proceso como el ciclo de vida de desarrollo de seguridad (SDL) normalmente requiere que los desarrolladores identifiquen los límites de confianza que existen en su aplicación. Existe un límite de confianza en lugares donde una aplicación puede interactuar con los datos que proporciona un contexto de confianza menor, como otro proceso en el sistema o un proceso de modo de usuario no administrativo, en el caso de un controlador de dispositivo en modo kernel. La nueva clase de vulnerabilidades que implican canales laterales de ejecución especulativa es relevante para muchos de los límites de confianza en los modelos de seguridad de software existentes que aíslan el código y los datos de un dispositivo.

En la tabla siguiente se proporciona un resumen de los modelos de seguridad de software en los que es posible que los desarrolladores deban preocuparse por estas vulnerabilidades:

Límite de confianza Descripción
Límite de máquina virtual Las aplicaciones que aíslan cargas de trabajo en máquinas virtuales independientes que reciben datos que no son de confianza de otra máquina virtual pueden estar en riesgo.
Límite de kernel Un controlador de dispositivo en modo kernel que recibe datos que no son de confianza de un proceso de modo de usuario no administrativo puede estar en riesgo.
Límite de proceso Una aplicación que recibe datos que no son de confianza de otro proceso que se ejecuta en el sistema local como, por ejemplo, mediante una llamada a un procedimiento remoto (RPC), memoria compartida u otros mecanismos de comunicación entre procesos (IPC) puede estar en riesgo.
Límite del enclave Una aplicación que se ejecuta en un enclave seguro (como Intel SGX) que recibe datos que no son de confianza desde fuera del enclave puede estar en riesgo.
Límite de lenguaje Una aplicación que interpreta, o compila y ejecuta código Just-In-Time (JIT) que no es de confianza escrito en un lenguaje de nivel superior puede estar en riesgo.

Las aplicaciones que tienen superficie expuesta a ataques en cualquiera de los límites de confianza anteriores deben revisar el código en la superficie expuesta a ataques para identificar y mitigar posibles instancias de vulnerabilidades de canal lateral de ejecución especulativa. Debe tenerse en cuenta que no se ha demostrado que los límites de confianza expuestos a superficies de ataque remotos, como los protocolos de red remotos, estén en riesgo para las vulnerabilidades de canal lateral de ejecución especulativa.

Patrones de codificación potencialmente vulnerables

Las vulnerabilidades de canal lateral de ejecución especulativa pueden surgir como consecuencia de varios patrones de codificación. En esta sección se describen patrones de codificación potencialmente vulnerables y se proporcionan ejemplos para cada uno de ellos, pero cabe señalar que pueden existir variaciones en estos temas. Por lo tanto, se recomienda a los desarrolladores tomar estos patrones como ejemplos y no como una lista exhaustiva de todos los patrones de codificación potencialmente vulnerables. Las mismas clases de vulnerabilidades de seguridad de memoria que pueden existir en el software en la actualidad también pueden existir a lo largo de rutas especulativas y desordenadas de ejecución, incluidas, entre otras, las saturaciones de búfer, los accesos de matriz fuera de los límites, el uso de memoria no inicializada, la confusión de tipos, etc. Los mismos primitivos que los atacantes pueden usar para aprovechar las vulnerabilidades de seguridad de memoria a lo largo de las rutas de acceso arquitectónicas también se pueden aplicar a las rutas especulativas.

En general, los canales laterales de ejecución especulativa relacionados con la falta de referencia de la rama condicional pueden surgir cuando una expresión condicional opera en datos que un contexto con menos confianza pueda controlar o en los que pueda ejercer influencia. Por ejemplo, esto puede incluir expresiones condicionales usadas en if, for, while, switch o en instrucciones ternarias. Para cada una de estas instrucciones, el compilador puede generar una rama condicional para la que la CPU pueda predecir el destino de la rama en tiempo de ejecución.

En cada ejemplo, se inserta un comentario con la frase "BARRERA ESPECULATIVA" donde un desarrollador podría introducir una barrera como mitigación. Esto se describe con más detalle en la sección sobre mitigaciones.

Carga especulativa fuera de los límites

Esta categoría de patrones de codificación implica un error de rama condicional que lleva a un acceso especulativo a la memoria fuera de los límites.

Carga de matriz fuera de los límites que alimenta una carga

Este es el patrón de codificación vulnerable descrito originalmente para CVE-2017-5753 (omisión de comprobación de límites). En la sección de antecedentes de este artículo se explica con detalle este patrón.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        // SPECULATION BARRIER
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

De forma similar, una carga de matriz fuera de los límites puede producirse junto con un bucle que supera su condición de terminación debido a un error. En este ejemplo, la rama condicional asociada a la expresión x < buffer_size puede producir un error y ejecutar especulativamente el cuerpo del bucle for cuando x es mayor o igual que buffer_size, lo que da lugar a una carga especulativa fuera de los límites.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadBytes(unsigned char *buffer, unsigned int buffer_size) {
    for (unsigned int x = 0; x < buffer_size; x++) {
        // SPECULATION BARRIER
        unsigned char value = buffer[x];
        return shared_buffer[value * 4096];
    }
}

Carga de matriz fuera de los límites que alimenta una rama indirecta

Este patrón de codificación implica el caso en el que un error de rama condicional puede dar lugar a un acceso fuera de los límites a una matriz de punteros de función que, después, lleva a una rama indirecta a la dirección de destino que se leyó fuera de los límites. En el fragmento de código siguiente se proporciona un ejemplo en el que se muestra esto.

En este ejemplo, se proporciona a DispatchMessage un identificador de mensaje que no es de confianza mediante el parámetro untrusted_message_id. Si untrusted_message_id es menor que MAX_MESSAGE_ID, se usa para indexar en una matriz de punteros de función y rama al destino de rama correspondiente. Este código es seguro a nivel de arquitectura, pero si la CPU produce un error en la rama condicional, podría dar lugar a que untrusted_message_id indexe DispatchTable cuando su valor sea mayor o igual que MAX_MESSAGE_ID, lo que provocaría un acceso fuera de los límites. Esto podría dar lugar a la ejecución especulativa de una dirección de destino de rama que se deriva más allá de los límites de la matriz, lo que podría provocar la divulgación de información en función del código que se ejecute de forma especulativa.

#define MAX_MESSAGE_ID 16

typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);

const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];

void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
    if (untrusted_message_id < MAX_MESSAGE_ID) {
        // SPECULATION BARRIER
        DispatchTable[untrusted_message_id](buffer, buffer_size);
    }
}

Al igual que sucede con el caso de una carga fuera de los límites de una matriz que alimenta otra carga, esta condición también puede surgir junto con un bucle que supera su condición de terminación debido a un error.

Almacén de matriz fuera de los límites que alimenta una rama indirecta

Aunque en el ejemplo anterior se ha mostrado cómo una carga especulativa fuera de los límites puede influir en un destino de rama indirecta, también es posible que un almacén fuera de los límites modifique un destino de rama indirecta como, por ejemplo, un puntero de función o una dirección de retorno. Esto puede provocar una ejecución especulativa desde una dirección especificada por el atacante.

En este ejemplo, se pasa un índice que no es de confianza mediante el parámetro untrusted_index. Si untrusted_index es menor que el recuento de elementos de la matriz pointers (256 elementos), el valor de puntero proporcionado en ptr se escribe en la matriz pointers. Este código es seguro a nivel de arquitectura, pero si la CPU produce un error en la rama condicional, podría provocar que se escriba ptr de forma especulativa más allá de los límites de la matriz pointers asignada a la pila. Esto podría dar lugar a daños especulativos de la dirección de devolución para WriteSlot. Si un atacante puede controlar el valor de ptr, puede provocar la ejecución especulativa desde una dirección arbitraria cuando WriteSlot vuelve por la ruta de acceso especulativa.

unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
    void *pointers[256];
    if (untrusted_index < 256) {
        // SPECULATION BARRIER
        pointers[untrusted_index] = ptr;
    }
}

De forma similar, si se asignó en la pila una variable local de puntero de función denominada func, es posible que se pueda modificar de forma especulativa la dirección a la que func hace referencia cuando se produce el error de rama condicional. Esto podría provocar la ejecución especulativa desde una dirección arbitraria cuando se llama al puntero de función.

unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
    void *pointers[256];
    void (*func)() = &callback;
    if (untrusted_index < 256) {
        // SPECULATION BARRIER
        pointers[untrusted_index] = ptr;
    }
    func();
}

Debe tenerse en cuenta que ambos ejemplos implican la modificación especulativa de punteros de rama indirectos asignados a la pila. Es posible que también se produzca una modificación especulativa para variables globales, memoria asignada al montón e incluso memoria de solo lectura en algunas CPU. En el caso de la memoria asignada a la pila, el compilador de Microsoft C++ ya toma medidas para que sea más difícil modificar de forma especulativa destinos de rama indirecta asignados a la pila, como reordenar variables locales, de modo que los búferes se colocan adyacentes a una cookie de seguridad como parte de la característica de seguridad del compilador /GS.

Confusión de tipo especulativo

Esta categoría se ocupa de patrones de codificación que pueden dar lugar a una confusión de tipo especulativo. Esto ocurre cuando se accede a la memoria mediante un tipo incorrecto a lo largo de una ruta de acceso no arquitectónica durante la ejecución especulativa. Tanto el error de rama condicional como la omisión del almacén especulativo pueden dar lugar a una confusión de tipo especulativo.

Para la omisión de almacenes especulativos, esto puede ocurrir en escenarios en los que un compilador reutiliza una ubicación de pila para variables de varios tipos. Esto se debe a que se puede omitir el almacén arquitectónico de una variable de tipo A, lo que permite que la carga del tipo A se ejecute de forma especulativa antes de asignar la variable. Si la variable almacenada anteriormente es de un tipo diferente, esto puede crear las condiciones para una confusión de tipo especulativo.

En el caso de un error de rama condicional, se usará el fragmento de código siguiente para describir condiciones diferentes a las que la confusión de tipo especulativo puede dar lugar.

enum TypeName {
    Type1,
    Type2
};

class CBaseType {
public:
    CBaseType(TypeName type) : type(type) {}
    TypeName type;
};

class CType1 : public CBaseType {
public:
    CType1() : CBaseType(Type1) {}
    char field1[256];
    unsigned char field2;
};

class CType2 : public CBaseType {
public:
    CType2() : CBaseType(Type2) {}
    void (*dispatch_routine)();
    unsigned char field2;
};

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ProcessType(CBaseType *obj)
{
    if (obj->type == Type1) {
        // SPECULATION BARRIER
        CType1 *obj1 = static_cast<CType1 *>(obj);

        unsigned char value = obj1->field2;

        return shared_buffer[value * 4096];
    }
    else if (obj->type == Type2) {
        // SPECULATION BARRIER
        CType2 *obj2 = static_cast<CType2 *>(obj);

        obj2->dispatch_routine();

        return obj2->field2;
    }
}

Confusión de tipo especulativo que conduce a una carga fuera de los límites

Este patrón de codificación implica el caso en el que una confusión de tipo especulativo puede dar lugar a un acceso de campo fuera de los límites o de tipo confuso donde el valor cargado alimenta una dirección de carga posterior. Esto es similar al patrón de codificación fuera de los límites de la matriz, pero se manifiesta mediante una secuencia de codificación alternativa, tal como se ha mostrado anteriormente. En este ejemplo, un contexto de ataque podría hacer que el contexto de la víctima ejecute ProcessType varias veces con un objeto de tipo CType1 (el campo type es igual a Type1). Esto tendrá el efecto de entrenar la rama condicional para que la primera instrucción if prediga que no se tome. Después, el contexto de ataque puede hacer que el contexto de la víctima ejecute ProcessType con un objeto de tipo CType2. Esto puede dar lugar a una confusión de tipo especulativo si la rama condicional de la primera instrucción if está en desuso y ejecuta el cuerpo de la instrucción if, lo que convierte un objeto de tipo CType2 en CType1. Dado que CType2 es menor que CType1, el acceso a memoria a CType1::field2 dará como resultado una carga especulativa fuera de los límites de datos que puede ser secreta. Después, este valor se usa en una carga desde shared_buffer, que puede crear efectos secundarios observables, como en el ejemplo de matriz fuera de los límites descrito anteriormente.

Confusión de tipo especulativo que lleva a una rama indirecta

Este patrón de codificación implica el caso en el que una confusión de tipo especulativo puede dar lugar a una rama indirecta no segura durante la ejecución especulativa. En este ejemplo, un contexto de ataque podría hacer que el contexto de la víctima ejecute ProcessType varias veces con un objeto de tipo CType2 (el campo type es igual a Type2). Esto tendrá el efecto de entrenar la rama condicional para que la primera instrucción if se tome y la instrucción else if no se tome. Después, el contexto de ataque puede hacer que el contexto de la víctima ejecute ProcessType con un objeto de tipo CType1. Esto puede dar lugar a una confusión de tipo especulativo si la rama condicional de la primera instrucción if predice que se toma y la instrucción else if predice que no se toma, lo que ejecuta el cuerpo de else if y convierte un objeto de tipo CType1 en CType2. Dado que el campo CType2::dispatch_routine se superpone con la matriz CType1::field1 de char, esto podría dar lugar a una rama indirecta especulativa en un destino de rama no previsto. Si el contexto de ataque puede controlar los valores de bytes de la matriz CType1::field1, es posible que puedan controlar la dirección de destino de la rama.

Uso especulativo no inicializado

Esta categoría de patrones de codificación implica escenarios en los que la ejecución especulativa puede acceder a la memoria no inicializada y usarla para alimentar una rama indirecta o una carga posterior. Para que estos patrones de codificación se puedan aprovechar, un atacante debe ser capaz de controlar el contenido de la memoria que se usa, o de influir significativamente en él, sin que la inicialice el contexto en el que se usa.

Uso especulativo no inicializado que conduce a una carga fuera de los límites

Un uso especulativo no inicializado puede provocar una carga fuera de los límites con un valor controlado por el atacante. En el ejemplo siguiente, el valor de index se asigna en trusted_index en todas las rutas de acceso arquitectónicas y se supone que trusted_index es menor o igual que buffer_size. Pero en función del código que genera el compilador, es posible que se produzca una omisión de almacén especulativo que permita que la carga de buffer[index] y las expresiones dependientes se ejecuten antes de la asignación a index. Si esto ocurre, se usará un valor no inicializado para index como desplazamiento en buffer, que podría permitir que un atacante lea información confidencial fuera de los límites y la transmita mediante un canal lateral por medio de la carga dependiente de shared_buffer.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

void InitializeIndex(unsigned int trusted_index, unsigned int *index) {
    *index = trusted_index;
}

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int trusted_index) {
    unsigned int index;

    InitializeIndex(trusted_index, &index); // not inlined

    // SPECULATION BARRIER
    unsigned char value = buffer[index];
    return shared_buffer[value * 4096];
}

Uso especulativo no inicializado que lleva a una rama indirecta

Un uso especulativo no inicializado posiblemente puede llevar a una rama indirecta en la que un atacante controla el destino de la rama. En el ejemplo siguiente, routine se asigna a DefaultMessageRoutine1 o DefaultMessageRoutine, en función del valor de mode. En la ruta de acceso arquitectónica, esto hará que routine siempre se inicialice antes de la rama indirecta. Pero en función del código que genera el compilador, puede producirse una omisión de almacén especulativo que permita que la rama indirecta se ejecute de forma especulativa mediante routine antes de la asignación a routine. Si esto ocurre, un atacante puede ejecutar de forma especulativa desde una dirección arbitraria, suponiendo que el atacante pueda influir en el valor no inicializado de routine, o controlarlo.

#define MAX_MESSAGE_ID 16

typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);

const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];
extern unsigned int mode;

void InitializeRoutine(MESSAGE_ROUTINE *routine) {
    if (mode == 1) {
        *routine = &DefaultMessageRoutine1;
    }
    else {
        *routine = &DefaultMessageRoutine;
    }
}

void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
    MESSAGE_ROUTINE routine;

    InitializeRoutine(&routine); // not inlined

    // SPECULATION BARRIER
    routine(buffer, buffer_size);
}

Opciones de mitigación

Las vulnerabilidades de canal lateral de ejecución especulativa se pueden mitigar realizando cambios en el código fuente. Estos cambios pueden implicar la mitigación de instancias específicas de una vulnerabilidad, tales como la incorporación de una barrera de especulación, o realizando cambios en el diseño de una aplicación a fin de que la información confidencial sea inaccesible para la ejecución especulativa.

Barrera de especulación mediante instrumentación manual

Un desarrollador puede insertar manualmente una barrera de especulación para evitar que la ejecución especulativa continúe a lo largo de una ruta de acceso no arquitectónica. Por ejemplo, un desarrollador puede insertar una barrera de especulación antes de un patrón de codificación peligroso en el cuerpo de un bloque condicional, ya sea al principio del bloque (después de la rama condicional) o antes de la primera carga que sea de preocupación. Esto impedirá que un error de rama condicional ejecute el código peligroso en una ruta de acceso no arquitectónica serializando la ejecución. La secuencia de barrera de especulación difiere según la arquitectura de hardware, tal como se describe en la tabla siguiente:

Architecture Intrínseco de barrera de especulación para CVE-2017-5753 Intrínseco de barrera de especulación para CVE-2018-3639
x86/x64 _mm_lfence() _mm_lfence()
ARM actualmente, no disponible __dsb(0)
ARM64 actualmente, no disponible __dsb(0)

Por ejemplo, el patrón de código siguiente se puede mitigar mediante el intrínseco _mm_lfence, tal como se muestra a continuación.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        _mm_lfence();
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

Barrera de especulación mediante instrumentación en tiempo de compilación

El compilador de Microsoft C++ en Visual Studio 2017 (a partir de la versión 15.5.5) incluye compatibilidad con el modificador /Qspectre que inserta automáticamente una barrera de especulación para un conjunto limitado de patrones de codificación potencialmente vulnerables relacionados con CVE-2017-5753. La documentación de la marca /Qspectre proporciona más información sobre sus efectos y uso. Es importante tener en cuenta que esta marca no cubre todos los patrones de codificación potencialmente vulnerables y, como tales, los desarrolladores no deben confiar en ella como una mitigación completa para esta clase de vulnerabilidades.

Enmascaramiento de índices de matriz

En los casos en los que se pueda producir una carga especulativa fuera de los límites, el índice de matriz se puede enlazar de forma sólida en la ruta de acceso arquitectónica y no arquitectónica agregando lógica para enlazar de forma explícita el índice de matriz. Por ejemplo, si una matriz se puede asignar a un tamaño que está alineado con una potencia de dos, se puede introducir una máscara sencilla. Este caso se muestra en el ejemplo siguiente, donde se supone que buffer_size está alineado con una potencia de dos. Esto garantiza que untrusted_index siempre sea menor que buffer_size, incluso si se produce un error en una rama condicional y untrusted_index se pasó con un valor mayor o igual que buffer_size.

Se debe tener en cuenta que el enmascaramiento de índices realizado aquí podría estar sujeto a omisión de almacén especulativo en función del código que ha generado el compilador.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        untrusted_index &= (buffer_size - 1);
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

Eliminación de información confidencial de la memoria

Otra técnica que se puede usar para mitigar las vulnerabilidades de canal lateral de ejecución especulativa consiste en quitar información confidencial de la memoria. Los desarrolladores de software pueden buscar oportunidades para refactorizar su aplicación de forma que la información confidencial no sea accesible durante la ejecución especulativa. Esto se puede lograr refactorizando el diseño de una aplicación para aislar la información confidencial en procesos independientes. Por ejemplo, una aplicación de explorador web puede intentar aislar los datos asociados a cada origen web en procesos independientes, lo que impide que un proceso pueda acceder a datos entre orígenes mediante la ejecución especulativa.

Consulte también

Instrucciones para mitigar las vulnerabilidades de canal lateral de ejecución especulativa
Mitigación de las vulnerabilidades de hardware de canal lateral de ejecución especulativa