Por que o gcc pode carregar especulativamente a partir de uma estrutura?

Exemplo mostrando a otimização do gcc e o código do usuário que podem falhar

A função 'foo' no snippet abaixo carregará apenas um dos membros da estrutura A ou B; bem, pelo menos essa é a intenção do código não otimizado.

typedef struct {
  int A;
  int B;
} Pair;

int foo(const Pair *P, int c) {
  int x;
  if (c)
    x = P->A;
  else
    x = P->B;
  return c/102 + x;
}

Aqui está o que o gcc -O3 fornece:

mov eax, esi
mov edx, -1600085855
test esi, esi
mov ecx, DWORD PTR [rdi+4]   <-- ***load P->B**
cmovne ecx, DWORD PTR [rdi]  <-- ***load P->A***
imul edx
lea eax, [rdx+rsi]
sar esi, 31
sar eax, 6
sub eax, esi
add eax, ecx
ret

Portanto, parece que o gcc pode carregar especulativamente os dois membros da estrutura, a fim de eliminar ramificações. Mas então, o código a seguir é considerado um comportamento indefinido ou a otimização do gcc acima é ilegal?

#include <stdlib.h>  

int naughty_caller(int c) {
  Pair *P = (Pair*)malloc(sizeof(Pair)-1); // *** Allocation is enough for A but not for B ***
  if (!P) return -1;

  P->A = 0x42; // *** Initializing allocation only where it is guaranteed to be allocated ***

  int res = foo(P, 1); // *** Passing c=1 to foo should ensure only P->A is accessed? ***

  free(P);
  return res;
}

Se a especulação de carga ocorrer no cenário acima, é possível que carregar P-> B cause uma exceção, pois o último byte de P-> B pode estar na memória não alocada. Essa exceção não ocorrerá se a otimização estiver desativada.

A questão

A otimização do gcc mostrada acima da especulação de carga é legal? Onde as especificações dizem ou sugerem que está tudo bem? Se a otimização é legal, como o código no 'naughtly_caller' acaba sendo um comportamento indefinido?

questionAnswers(6)

yourAnswerToTheQuestion