Diretrizes para desenvolvedores de C++ para canais laterais de execução especulativa

Este artigo contém diretrizes para ajudar os desenvolvedores a identificar e mitigar vulnerabilidades de hardware de canais laterais de execução especulativa no software C++. Essas vulnerabilidades podem revelar informações confidenciais entre limites de confiança e podem afetar o software executado em processadores que dão suporte à execução especulativa e fora de ordem de instruções. Essa classe de vulnerabilidades foi descrita pela primeira vez em janeiro de 2018 e podem ser encontradas diretrizes e informações adicionais no Aviso de segurança da Microsoft.

As diretrizes fornecidas por este artigo estão relacionadas às classes de vulnerabilidades representadas por:

  1. CVE-2017-5753, também conhecido como Spectre variant 1. Essa classe de vulnerabilidade de hardware está relacionada a canais laterais que podem surgir devido à execução especulativa que ocorre como resultado de uma má avaliação condicional do branch. O compilador do Microsoft C++ no Visual Studio 2017 (a partir da versão 15.5.5) inclui suporte para a opção /Qspectre, que fornece uma mitigação em tempo de compilação para um conjunto limitado de padrões de codificação potencialmente vulneráveis relacionados à CVE-2017-5753. A opção /Qspectre também está disponível no Visual Studio 2015 Atualização 3 por meio do KB 4338871. A documentação do sinalizador /Qspectre fornece mais informações sobre seus efeitos e uso.

  2. CVE-2018-3639, também conhecido como Bypass de Repositório Especulativo (SSB). Essa classe de vulnerabilidade de hardware está relacionada a canais laterais que podem surgir devido à execução especulativa de uma carga à frente de um repositório dependente, como resultado de uma má avaliação do acesso à memória.

Uma introdução acessível às vulnerabilidades de canais laterais de execução especulativa pode ser encontrada na apresentação intitulada O Caso de Spectre e Meltdown por uma das equipes de pesquisa que descobriu esses problemas.

O que são vulnerabilidades de hardware do Canal Lateral de Execução Especulativa?

As CPUs modernas fornecem graus mais altos de desempenho usando a execução especulativa e fora de ordem de instruções. Por exemplo, isso geralmente é feito prevendo o destino de branches (condicional e indireto) que permite que a CPU comece a executar instruções especulativamente no destino de branch previsto, evitando assim uma parada até que o destino real do branch seja resolvido. No caso de a CPU descobrir posteriormente que ocorreu uma má interpretação, todo o estado do computador que foi calculado especulativamente será descartado. Isso garante que não haja efeitos arquitetonicamente visíveis da especulação mal avaliada.

Embora a execução especulativa não afete o estado arquitetonicamente visível, ela pode deixar rastreamentos residuais em estado não arquitetônico, como os vários caches usados pela CPU. São esses rastreamentos residuais de execução especulativa que podem gerar vulnerabilidades de canal lateral. Para entender melhor isso, considere o fragmento de código a seguir, que fornece um exemplo de CVE-2017-5753 (Bypass de verificação de limites):

// 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];
    }
}

Neste exemplo, ReadByte é fornecido com um buffer, um tamanho de buffer e um índice no buffer. O parâmetro de índice, conforme especificado por untrusted_index, é fornecido por um contexto menos privilegiado, como um processo não administrativo. Se untrusted_index for menor que buffer_size, o caractere nesse índice será lido de buffer e usado para indexar em uma região compartilhada de memória referenciada por shared_buffer.

De uma perspectiva arquitetônica, essa sequência de códigos é perfeitamente segura, pois é garantido que untrusted_index sempre será menor que buffer_size. No entanto, na presença de execução especulativa, é possível que a CPU avalie incorretamente o branch condicional e execute o corpo da instrução if mesmo quando untrusted_index for maior ou igual a buffer_size. Como consequência disso, a CPU pode ler especulativamente um byte além dos limites de buffer (que pode ser um segredo) e, em seguida, usar esse valor de byte para calcular o endereço de uma carga subsequente por meio de shared_buffer.

Embora a CPU acabe detectando essa configuração incorreta, efeitos colaterais residuais podem ser deixados no cache da CPU que revelem informações sobre o valor do byte que foi lido fora dos limites de buffer. Esses efeitos colaterais podem ser detectados por um contexto menos privilegiado em execução no sistema examinando a rapidez com que cada linha de cache de shared_buffer é acessada. As etapas que podem ser realizadas para fazer isso são:

  1. Invocar ReadByte várias vezes com untrusted_index sendo menor que buffer_size. O contexto de ataque pode fazer com que o contexto da vítima invoque ReadByte (por exemplo, via RPC) de modo que a previsão do branch seja treinada para não ser tomada, pois untrusted_index é menor que buffer_size.

  2. Liberar todas as linhas de cache em shared_buffer. O contexto de ataque deve liberar todas as linhas de cache na região compartilhada da memória referenciada por shared_buffer. Como a região de memória é compartilhada, isso é simples e pode ser feito usando intrínsecos como _mm_clflush.

  3. Invocar ReadByte com untrusted_index sendo maior que buffer_size. O contexto de ataque faz com que o contexto da vítima invoque ReadByte de modo que ele preveja incorretamente que o branch não será tomado. Isso faz com que o processador execute especulativamente o corpo do bloco if com untrusted_index maior que buffer_size, levando a uma leitura fora dos limites de buffer. Consequentemente, shared_buffer é indexado usando um valor potencialmente secreto que foi lido fora dos limites, fazendo com que a respectiva linha de cache seja carregada pela CPU.

  4. Ler cada linha de cache em shared_buffer para ver o que é acessado mais rapidamente. O contexto de ataque pode ler cada linha de cache em shared_buffer e detectar a linha de cache que é carregada significativamente mais rápido do que as outras. Essa é a linha de cache que provavelmente foi trazida pela etapa 3. Como há uma relação 1:1 entre o valor de byte e a linha de cache neste exemplo, isso permite ao invasor inferir o valor real do byte que foi lido fora dos limites.

As etapas acima fornecem um exemplo de uso de uma técnica conhecida como FLUSH+RELOAD em conjunto com a exploração de uma instância do CVE-2017-5753.

Quais cenários de software podem ser afetados?

O desenvolvimento de software seguro usando um processo como o SDL (Ciclo de Vida de Desenvolvimento de Segurança) normalmente exige que os desenvolvedores identifiquem os limites de confiança existentes em seu aplicativo. Existe um limite de confiança em locais em que um aplicativo pode interagir com dados fornecidos por um contexto menos confiável, como outro processo no sistema ou um processo de modo de usuário não administrativo no caso de um driver de dispositivo no modo kernel. A nova classe de vulnerabilidades que envolvem canais laterais de execução especulativa é relevante para muitos dos limites de confiança em modelos de segurança de software existentes, que isolam código e dados em um dispositivo.

A tabela a seguir fornece um resumo dos modelos de segurança de software em que os desenvolvedores podem precisar se preocupar com a ocorrência dessas vulnerabilidades:

Limite de confiança Descrição
Limite de máquina virtual Aplicativos que isolam cargas de trabalho em máquinas virtuais separadas, que recebem dados não confiáveis de outra máquina virtual, podem estar em risco.
Limite de kernel Um driver de dispositivo no modo kernel que recebe dados não confiáveis de um processo de modo de usuário não administrativo pode estar em risco.
Limite de processo Um aplicativo que recebe dados não confiáveis de outro processo em execução no sistema local, como por meio de uma RPC (Chamada de Procedimento Remoto), memória compartilhada ou outros mecanismos de comunicação Inter-Process (IPC) pode estar em risco.
Limite do enclave Um aplicativo que é executado em um enclave seguro (como o Intel SGX) que recebe dados não confiáveis de fora do enclave pode estar em risco.
Limite de linguagem de programação Um aplicativo que interpreta ou JIT (Just-In-Time) compila e executa código não confiável escrito em uma linguagem de nível superior pode estar em risco.

Aplicativos que têm a superfície de ataque exposta a qualquer um dos limites de confiança acima devem examinar o código na superfície de ataque para identificar e atenuar possíveis instâncias de vulnerabilidades de canal lateral de execução especulativa. Deve-se observar que os limites de confiança expostos a superfícies de ataque remoto, como protocolos de rede remota, não foram demonstrados como em risco para vulnerabilidades de canal lateral de execução especulativa.

Padrões de codificação potencialmente vulneráveis

Vulnerabilidades de canal lateral de execução especulativa podem surgir como consequência de vários padrões de codificação. Esta seção descreve padrões de codificação potencialmente vulneráveis e fornece exemplos para cada um, mas é importante reconhecer que podem existir variações nesses temas. Assim, os desenvolvedores são aconselhados a usar esses padrões como exemplos e não como uma lista completa de todos os padrões de codificação potencialmente vulneráveis. As mesmas classes de vulnerabilidades de segurança de memória que podem existir no software hoje também podem existir ao longo de caminhos especulativos e fora de ordem de execução, incluindo, mas não se limitando a, a sobrecargas de buffer, acessos de matriz fora dos limites, uso de memória não inicializada, confusão de tipos e assim por diante. Os mesmos primitivos que os invasores podem usar para explorar vulnerabilidades de segurança de memória ao longo de caminhos arquitetônicos também podem se aplicar a caminhos especulativos.

Em geral, os canais laterais de execução especulativa relacionados à má avaliação condicional do branch podem surgir quando uma expressão condicional opera em dados que podem ser controlados ou influenciados por um contexto menos confiável. Por exemplo, isso pode incluir expressões condicionais usadas em instruções if, for, while, switch ou ternárias. Para cada uma dessas instruções, o compilador pode gerar um branch condicional para o qual a CPU pode prever o destino do branch em runtime.

Para cada exemplo, é inserido um comentário com a frase "SPECULATION BARRIER" em que um desenvolvedor pode introduzir uma barreira como mitigação. Isso é discutido mais detalhadamente na seção sobre mitigações.

Carga especulativa fora dos limites

Essa categoria de padrões de codificação envolve uma má avaliação condicional de ramificação que leva a um acesso especulativo de memória fora dos limites.

Carga fora dos limites da matriz alimentando uma carga

Esse padrão de codificação é o padrão de codificação vulnerável descrito originalmente para CVE-2017-5753 (Bypass de Verificação de Limites). A seção em segundo plano deste artigo explica esse padrão em detalhes.

// 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];
    }
}

Da mesma forma, uma carga fora dos limites da matriz pode ocorrer em conjunto com um loop que excede sua condição de término devido a um erro de previsibilidade. Neste exemplo, o branch condicional associado à expressão x < buffer_size pode executar de maneira incorreta e especulativa o corpo do loop for quando x for maior ou igual a buffer_size, resultando em uma carga especulativa fora dos limites.

// 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 fora dos limites da matriz alimentando um branch indireto

Esse padrão de codificação envolve o caso em que uma avaliação incorreta de branch condicional pode levar a um acesso fora dos limites de uma matriz de ponteiros de função que, em seguida, leva a um branch indireto para o endereço de destino que foi lido fora dos limites. O snippet a seguir fornece um exemplo que demonstra isso.

Neste exemplo, um identificador de mensagem não confiável é fornecido ao DispatchMessage por meio do parâmetro untrusted_message_id. Se untrusted_message_id for menor que MAX_MESSAGE_ID, ele será usado para indexar em uma matriz de ponteiros de função e ramificar para o destino de branch correspondente. A arquitetura desse código o torna seguro, mas, se a CPU avaliar o branch condicional incorretamente, poderá resultar na indexação de DispatchTable por untrusted_message_id, quando seu valor for maior ou igual a MAX_MESSAGE_ID, levando a um acesso fora dos limites. Isso pode resultar em execução especulativa de um endereço de destino de branch derivado além dos limites da matriz, o que pode levar à divulgação de informações dependendo do código executado especulativamente.

#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);
    }
}

Assim como acontece com o caso de uma carga fora dos limites da matriz alimentando outra carga, essa condição também pode surgir em conjunto com um loop que exceda sua condição de término devido a uma má avaliação.

Repositório fora dos limites da matriz alimentando um branch indireto

Embora o exemplo anterior tenha mostrado como uma carga externa especulativa pode influenciar um destino de branch indireto, também é possível que um repositório fora dos limites modifique um destino de branch indireto, como um ponteiro de função ou um endereço de retorno. Isso pode potencialmente levar à execução especulativa de um endereço especificado pelo invasor.

Neste exemplo, um índice não confiável é passado pelo parâmetro untrusted_index. Se untrusted_index for menor que a contagem de elementos da matriz pointers (256 elementos), o valor do ponteiro fornecido em ptr será gravado na matriz pointers. Esse código é seguro de forma arquitetônica, mas se a CPU avaliar incorretamente o branch condicional, poderá resultar em ptr sendo gravado especulativamente além dos limites da matriz alocada em pilha pointers. Isso pode levar à corrupção especulativa do endereço de retorno para WriteSlot. Se um invasor puder controlar o valor de ptr, poderá causar execução especulativa de um endereço arbitrário quando WriteSlot retornar ao longo do caminho especulativo.

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

Da mesma forma, se uma variável local de ponteiro de função com o nome de func tiver sido alocada na pilha, talvez seja possível modificar especulativamente o endereço que func referencia a quando ocorre a avaliação incorreta do branch condicional. Isso pode resultar na execução especulativa de um endereço arbitrário quando o ponteiro da função é chamado.

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

Deve-se observar que ambos os exemplos envolvem modificação especulativa de ponteiros de branch indireto alocados em pilha. É possível que a modificação especulativa também possa ocorrer para variáveis globais, memória alocada por heap e até mesmo para memória somente leitura em algumas CPUs. Para memória alocada em pilha, o compilador do Microsoft C++ já executa etapas para tornar mais difícil modificar especulativamente destinos de branch indireto alocados em pilha, como reordenação de variáveis locais, de modo que os buffers sejam colocados adjacentes a um cookie de segurança como parte do recurso de segurança do compilador /GS.

Confusão de tipo especulativo

Essa categoria lida com padrões de codificação que podem gerar uma confusão de tipo especulativo. Isso ocorre quando a memória é acessada usando um tipo incorreto ao longo de um caminho não arquitetônico durante a execução especulativa. Tanto a avaliação incorreta do branch condicional quanto o bypass especulativo do repositório podem potencialmente levar a uma confusão de tipo especulativo.

Para bypass de repositório especulativo, isso pode ocorrer em cenários em que um compilador reutiliza um local de pilha para variáveis de vários tipos. Isso ocorre porque o repositório de arquitetura de uma variável de tipo A pode ser ignorado, permitindo assim que a carga do tipo A seja executada especulativamente antes que a variável seja atribuída. Se a variável armazenada anteriormente for de um tipo diferente, isso poderá criar as condições para uma confusão de tipo especulativo.

Para avaliação incorreta do branch condicional, o snippet de código a seguir será usado para descrever diferentes condições às quais a confusão de tipo especulativo pode dar origem.

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;
    }
}

Confusão de tipo especulativo que leva a uma carga fora dos limites

Esse padrão de codificação envolve o caso em que uma confusão de tipo especulativo pode resultar em um acesso de campo fora dos limites ou de tipo confuso em que o valor carregado alimenta um endereço de carga subsequente. Isso é semelhante ao padrão de codificação fora dos limites da matriz, mas é manifestado por meio de uma sequência de codificação alternativa, conforme mostrado acima. Neste exemplo, um contexto de ataque pode fazer com que o contexto da vítima execute ProcessType várias vezes com um objeto de tipo CType1 (o campo type é igual a Type1). Isso terá o efeito de treinar o branch condicional para que a primeira instrução if seja avaliada para não ser executada. O contexto de ataque pode fazer com que o contexto da vítima execute ProcessType com um objeto do tipo CType2. Isso pode resultar em uma confusão de tipo especulativo se o branch condicional da primeira instrução if for avaliada incorretamente e executar o corpo da instrução if, convertendo assim um objeto do tipo CType2 em CType1. Como CType2 é menor que CType1, o acesso à memória de CType1::field2 resultará em uma carga especulativa fora dos limites de dados que pode ser secreta. Esse valor é então usado em uma carga de shared_buffer, que pode criar efeitos colaterais observáveis, como com o exemplo fora dos limites da matriz descrito anteriormente.

Confusão de tipo especulativo que leva a um branch indireto

Esse padrão de codificação envolve o caso em que uma confusão de tipo especulativo pode resultar em um branch indireto não seguro durante a execução especulativa. Neste exemplo, um contexto de ataque pode fazer com que o contexto da vítima execute ProcessType várias vezes com um objeto de tipo CType2 (o campo type é igual a Type2). Isso terá o efeito de treinar o branch condicional para que a primeira instrução if seja avaliada para ser executada e a instrução else if não seja executada. O contexto de ataque pode fazer com que o contexto da vítima execute ProcessType com um objeto do tipo CType1. Isso pode resultar em uma confusão de tipo especulativo se o branch condicional para a primeira instrução if prever tomada e a instrução else if não for tomada, executando assim o corpo de else if e lançando um objeto de tipo CType1 para CType2. Como o campo CType2::dispatch_routine se sobrepõe a char matriz CType1::field1, isso pode resultar em um branch indireto especulativo para um destino de branch não intencional. Se o contexto de ataque puder controlar os valores de bytes na matriz CType1::field1, eles poderão controlar o endereço de destino do branch.

Uso não inicializado especulativo

Essa categoria de padrões de codificação envolve cenários em que a execução especulativa pode acessar a memória não inicializada e usá-la para alimentar uma carga subsequente ou um branch indireto. Para que esses padrões de codificação sejam exploráveis, um invasor precisa ser capaz de controlar ou influenciar significativamente o conteúdo da memória que é usada sem ser inicializado pelo contexto em que está sendo usado.

Uso não inicializado especulativo que leva a uma carga fora dos limites

Um uso não inicializado especulativo pode potencialmente levar a uma carga fora dos limites usando um valor controlado por um invasor. No exemplo abaixo, o valor de index é atribuído a trusted_index em todos os caminhos arquitetônicos e trusted_index é considerado menor ou igual a buffer_size. No entanto, dependendo do código produzido pelo compilador, é possível que ocorra um bypass de repositório especulativo que permita que a carga de buffer[index] e as expressões dependentes sejam executadas antes da atribuição de index. Se isso ocorrer, um valor não inicializado de index será usado como o deslocamento para buffer, que poderá permitir que um invasor leia informações confidenciais fora dos limites e as transmita por meio de um canal lateral por meio da carga dependente 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 não inicializado especulativo que leva a um branch indireto

Um uso especulativo não inicializado pode potencialmente levar a um branch indireto em que o destino do branch é controlado por um invasor. No exemplo abaixo, routine é atribuído a um DefaultMessageRoutine1 ou DefaultMessageRoutine, dependendo do valor de mode. No caminho arquitetônico, isso resultará que routine seja sempre inicializado antes do branch indireto. No entanto, dependendo do código produzido pelo compilador, pode ocorrer um bypass especulativo do repositório que permite que o branch indireto routine seja executado especulativamente antes da atribuição a routine. Se isso ocorrer, um invasor talvez consiga executar especulativamente a partir de um endereço arbitrário, supondo que o invasor possa influenciar ou controlar o valor não inicializado de routine.

#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);
}

Opções de mitigação

As vulnerabilidades de canal lateral de execução especulativa podem ser atenuadas fazendo alterações no código-fonte. Essas alterações podem envolver a mitigação de instâncias específicas de uma vulnerabilidade, como adicionar uma barreira de especulação ou fazer alterações no design de um aplicativo para tornar informações confidenciais inacessíveis à execução especulativa.

Barreira de especulação por meio da instrumentação manual

Uma barreira de especulação pode ser inserida manualmente por um desenvolvedor para impedir que a execução especulativa prossiga em um caminho não arquitetônico. Por exemplo, um desenvolvedor pode inserir uma barreira de especulação antes de um padrão de codificação perigoso no corpo de um bloco condicional, seja no início do bloco (após o branch condicional) ou antes da primeira carga que seja preocupante. Isso impedirá que uma avaliação incorreta de branch condicional execute o código perigoso em um caminho não arquitetônico serializando a execução. A sequência de barreira de especulação difere pela arquitetura de hardware, conforme descrito na tabela a seguir:

Arquitetura Barreira de especulação intrínseca para CVE-2017-5753 Barreira de especulação intrínseca para CVE-2018-3639
x86/x64 _mm_lfence() _mm_lfence()
ARM não disponível atualmente __dsb(0)
ARM64 não disponível atualmente __dsb(0)

Por exemplo, o padrão de código a seguir pode ser atenuado usando o _mm_lfence intrínseco, conforme mostrado abaixo.

// 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];
    }
}

Barreira de especulação por meio da instrumentação de tempo do compilador

O compilador do Microsoft C++ no Visual Studio 2017 (a partir da versão 15.5.5) inclui suporte para a opção /Qspectre, que insere automaticamente uma barreira de especulação para um conjunto limitado de padrões de codificação potencialmente vulneráveis relacionados ao CVE-2017-5753. A documentação do sinalizador /Qspectre fornece mais informações sobre seus efeitos e uso. É importante observar que esse sinalizador não abrange todos os padrões de codificação potencialmente vulneráveis e, como tal, os desenvolvedores não devem confiar nele como uma mitigação abrangente para essa classe de vulnerabilidades.

Índices de matriz de mascaramento

Nos casos em que uma carga fora dos limites especulativa pode ocorrer, o índice de matriz pode ser fortemente limitado ao caminho arquitetônico e não arquitetônico adicionando lógica para associar explicitamente o índice de matriz. Por exemplo, se uma matriz puder ser alocada a um tamanho alinhado a uma potência de dois, poderá ser introduzida uma máscara simples. Isso é ilustrado na amostra abaixo em que supõe-se que buffer_size esteja alinhado a uma potência de dois. Isso garante que untrusted_index seja sempre menor que buffer_size, mesmo que ocorra um erro de avaliação de branch condicional e untrusted_index tenha sido passado com um valor maior ou igual a buffer_size.

Deve-se observar que a máscara de índice executada aqui pode estar sujeita a bypass de repositório especulativo, dependendo do código gerado pelo 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];
    }
}

Removendo informações confidenciais da memória

Outra técnica que pode ser usada para atenuar vulnerabilidades de canal lateral de execução especulativa é remover informações confidenciais da memória. Os desenvolvedores de software podem procurar oportunidades para refatorar seu aplicativo, de modo que informações confidenciais não sejam acessíveis durante a execução especulativa. Isso pode ser feito refatorando o design de um aplicativo para isolar informações confidenciais em processos separados. Por exemplo, um aplicativo de navegador da Web pode tentar isolar os dados associados a cada origem da Web em processos separados, impedindo assim que um processo seja capaz de acessar dados entre origens por meio da execução especulativa.

Confira também

Diretrizes para atenuar as vulnerabilidades de canal lateral de execução especulativa
Mitigando vulnerabilidades de hardware do canal lateral de execução especulativa