Explicación del fallo de seguridad Meltdown y Spectre en los microprocesadores Intel

Escrito por el , actualizado el .
hardware planeta-codigo seguridad software
Enlace permanente Comentarios

Los procesadores Intel se han visto afectados por un grave error de seguridad debido a que fueron diseñados con ejecución especulativa sin tener algunas consideraciones de seguridad, técnica empleada para aumentar el rendimiento pero que tiene efectos colaterales en la cache que pueden se aprovechados para realizar ataques side-channel con los que leer el contenido de la memoria del kernel, independientemente del sistema operativo utilizado.

El año 2018 ha empezando haciéndose público uno de los peores bugs de seguridad que afecta a absolutamente todos los procesadores Intel que esta compañía ha fabricado en la última década, denominado Meltdown y su variante Spectre, el error tiene su propio nombre, logotipo y página web. Un error de diseño en los procesadores que solo se puede corregir reemplazando el microprocesador o modificando los sistemas operativos aunque se especula con una pérdida de rendimiento en ciertas cargas de trabajo, entre un 5% y un 30%. El error es tan grave que permite leer a un programa la memoria del núcleo del sistema operativo que debería estar protegida. En la memoria del kernel residen las claves de acceso a sistemas o datos sensibles que obtenidos y utilizados pueden ocasionar graves problemas de seguridad con consecuencias económicas o de acceso no autorizado a información. Este error es tan grave que deja al viejo conocido fallo de la división de los Pentium a la altura de chiste. Hace unos meses por si fuera poco se conocía otro error de seguridad en el Management Engine (ME) de Intel.

Meltdown Spectre

Logotipos de Meltdown y Spectre

Reemplazar todos los microprocesadores es tremendamente caro además de que primero hay que diseñar y fabricar unos que estén exentos del bug que lleva tiempo, meses o años hasta que estén preparados, por lo que la solución hasta el momento pasa por hacer modificaciones en el software y en los sistemas operativos, compiladores y programas para resolver o mitigar el problema. En el sistema operativo la solución consiste en separar el espacio de direcciones del kernel de la de los programas, sin embargo, cada vez que el microprocesador cambia entre un espacio de direcciones a otro hay una penalización en tiempo por lo que en ciertas cargas de trabajo muy intensivas en las que se cambia frecuentemente de contextos como operaciones de red, de almacenamiento rápido o E/S el rendimiento se ve afectado. Para un usuario doméstico, ofimático o juegos la perdida de rendimiento será insignificante y no será apreciable ya que en estos casos el microprocesador no trabaja a la máxima carga o no está cambiando frecuentemente del espacio de direcciones del kernel al de usuario. En grandes centros de datos como la computación en la nube de Amazon Web Services, Google Cloud Platform o Microsoft Azure el rendimiento será más apreciable.

Como usuarios domésticos para estar protegidos conviene descargar únicamente software de fuentes confiables pero para los usuarios empresariales con sus servicios en la nube en donde los sistemas están aislados pero usando infraestructura compartida y con el descubrimiento de este bug es más grave si no se parchea, los proveedores de infraestructura en la nube ya han planificando tareas de mantenimiento y reinicios obligatorios.

Intel AMD ARM

Técnicas para aumentar el rendimiento

Los microprocesadores modernos implementan varias técnicas para aumentar el rendimiento. Una de las mas simples es aumentar la frecuencia de trabajo del microprocesador, uno de los primeros Pentium trabajaba únicamente a 100 Mhz y los actuales llegan hasta los 3 Ghz, casi 30 veces más. Pero aumentar la frecuencia solo es posible hasta cierto límite a partir del cual el microprocesador se calienta mucho y consume mucha energía. Por lo que hay que emplear otras técnicas al mismo tiempo.

Otra de las mas simples es reducir el tamaño de los transistores, unos transistores más pequeños hace que sea posible incluir más transistores en el mismo espacio físico para incluir caches de mayor tamaño o nuevas funcionalidades, con más velocidad y con menor consumo de energía. El tamaño de los transistores de los Pentium originales era de 800 nanómetros e incluía 3.1 millones, los Intel Core de octava generación se fabrican a 14 nanómetros incluyendo unos 5000 millones, unas 60 veces más pequeños. Aún así cada vez es más difícil cumplir con la ley de Moore ya que se está llegando a límite físico de los átomos de los materiales, consistía en que cada dos años se duplica el número de transistores de un microprocesador.

Con la ayuda de unos transistores más pequeños y más espacio se aprovecha para aumentar el rendimiento incluyendo más núcleos de cómputo. Pero para aumentar el rendimiento de un núcleo de cómputo individual o el IPC se emplean otras técnicas como utilizar múltiples pipelines para ejecutar varias instrucciones simultáneamente, ejecución fuera de orden para reorganizar las instrucciones y la ejecución especulativa para mantener llenos esos pipelines.

Escalar

En un microprocesador escalar se ejecuta una instrucción por ciclo, por ejemplo, en esta secuencia de instrucciones que realizan unas sumas se tardarían 6 ciclos de reloj. A estos microprocesadores que ejecutan una instrucción por ciclo de reloj se les denomina escalares, siendo ejemplos el Intel 486 y el ARM1176 usado en la Raspberry Pi 1.

1
2
3
4
5
6
m = a+b
n = c+d
o = d+e
x = f+o
y = h+i
z = j+k
escalar.py

Superescalar

En un microprocesador con dos pipelines o superescalar se pueden realizar varias operaciones simultáneamente, es decir, mientras se realiza la primera operación en la variable m se realiza al mismo tiempo la segunda operación de n, con lo que estas operaciones podrían completarse en únicamente tres ciclos de reloj con la siguiente equivalencia de programa. Ejemplos de microprocesadores superescalares son el Intel Pentium y los ARM Cortex-A7 y Cortex-A53 estos últimos usados en la Raspberry Pi 2 y 3 respectivamente.

1
2
3
m, n = a+b, c+d
o, x = d+e, f+o
y, z = h+i, j+k
superescalar-1.py

Sin embargo, hacer la suma de o y x al mismo tiempo no es posible ya que antes de calcular x hay que calcular o debido a que uno de los operandos en la suma de x es o, es decir, hay una dependencia en estas instrucciones y se han de ejecutar una después de otra. Con lo que en vez de tres ciclos habría que conformase en ejecutar estas instrucciones en cuatro ciclos.

1
2
3
4
m, n = a+b, c+d 
o = d+e         # el segundo pipeline está ocioso
x, y = f+o, h+i 
z = j+k         # el segundo pipeline está ocioso
suprescalar-2.py

Fuera de orden

Los microprocesadores fuera de orden reordenan las instrucciones de la forma adecuada para que el programa sea equivalente pero manteniendo los pipelines llenos. Cambiando el orden entre las instrucciones x e y se consigue ejecutar las instrucciones en tres ciclos de reloj. Ejemplos de microprocesadores fuera de orden son el Pentium 2 y siguientes microprocesadores Intel y AMD incluyendo varios ARM Cortex-A9, A15, A17 y A57.

1
2
3
m, n = a+b, c+d
o, y = d+e, h+i
x, z = f+o, j+k
fuera-de-orden.py

Predicción de salto y ejecución especulativa

Los programas incluyen saltos con sentencias condicionales if o de bucle. Los microprocesadores tratan de adivinar si una sentencia de salto se producirá o no (con heurísticas y son bastante buenos acertando) para recuperar y tener preparadas las siguientes instrucciones. Mantener los pipelines llenos es difícil al aumentar su número a tres o cuatro. Para tratar de mantenerlos llenos los microprocesadores usan la predicción de salto y van ejecutando las instrucciones desechando las operaciones si finalmente no se acierta en el salto pero habiendo aumentado el rendimiento si se ha acertado, realizan ejecución especulativa de las instrucciones.

En este otro caso, v depende de u y u depende de t de modo que un microprocesador superescalar sin ejecución especulativa tardará tres ciclos computando t, u y v para determinar el valor de v en la sentencia condicional if (en otro ciclo) momento en que pasa otros tres ciclos calculando w, x e y, en total 4 o 7 ciclos dependiendo de si hay salto en la sentencia condicional.

1
2
3
4
5
6
7
t = a+b
u = t+c
v = u+d
if v:
   w = e+f
   x = w+g
   y = x+h
ejecucion-especulativa-1.py

Si el predictor de salto determina que es probable que la condición sea cierta la ejecución especulativa reordena el programa de la siguiente manera:

1
2
3
4
5
6
7
8
t = a+b
u = t+c
v = u+d
w_ = e+f
x_ = w_+g
y_ = x_+h
if v:
   w, x, y = w_, x_, y_
ejecucion-especulativa-2.py

Y con la ejecución superescalar se mantiene los pipelines ocupados de modo que el ejemplo tiene la siguiente equivalencia y tardando aproximadamente 3 ciclos cuando antes se necesitaban 7.

1
2
3
4
5
t, w_ = a+b, e+f
u, x_ = t+c, w_+g
v, y_ = u+d, x_+h
if v:
   w, x, y = w_, x_, y_
ejecucion-especulativa-3.py

Cache

Los microprocesadores son muy rápidos comparados con la memoria o el acceso al almacenamiento secundario. Un Cortex-A53 de una Raspberry Pi puede ejecutar una instrucción en 0.5 nanosegundos pero el acceso a memoria costar 100 nanosegundos. Esto no es bueno pero por fortuna los accesos a memoria siguen patrones, accediendo repetidamente a variables recientemente accedidas y accediendo a variables en posiciones cercanas, de forma que colocando estas variables en una cache más rápida y cercana al procesador que la memoria principal se mitiga en gran medida el problema.

Relación entre ejecución especulativa, cache y Meltdown y Spectre

La ejecución especulativa tiene el efecto colateral de colocar datos en la memoria cache del microprocesador y esto es utilizado para realizar una forma de ataque side-channel. Desde el punto de vista de Meltdown y Spectre y la ejecución especulativa lo importante es que midiendo el tiempo que tarda el acceso a memoria se puede conocer si el dato está en la cache (tarda poco) o no (tarda mucho).

1
2
3
4
5
6
7
t = a+b
u = t+c
v = u+d
if v:
   w = kern_mem[address]   # si se llega aquí se produce un fallo
   x = w & 0x100           # operación de bit and
   y = user_mem[x]
meltdown-1.py

u tiene una dependencia sobre t y v sobre u con lo que el microprocesador usando la superescalabilidad, la ejecución fuera de orden y ejecución especulativa acabaría transformando el programa en la siguiente secuencia de operaciones:

1
2
3
4
5
t, w_ = a+b, kern_mem[address]
u, x_ = t+c, w_ & 0x100
v, y_ = u+d, user_mem[x_]
if v:
   w, x, y = w_, x_, y_      # nunca se llega aquí, si no fallo
meltdown-2.py

El microprocesador lee de el valor de una dirección del kernel de forma especulativa pero el fallo en la operación de acceso no se produce hasta se conoce el valor de v utilizando en la sentencia condicional no es cero. Limpiando la cache previamente y haciendo que v de cero para que no se produzca la excepción con los valores adecuados de las variables (a, b, c, d) la ejecución especulativa de v, y_ = u+d, user_mem[x_] producirá un acceso a la dirección de memoria 0x000 o 0x100 dependiendo del valor del octavo bit recuperado en el acceso ilegal a la dirección de memoria kern_mem[address]. El ataque side-channel se produce midiendo el tiempo que tarda una instrucción posterior que utilice estas direcciones, si está o no está en la cache (por el tiempo que tarda) determina a que dirección de memoria se ha accedido y cual es el valor del octavo bit de una dirección del kernel. ¡Felicidades has leído un bit de la memoria del kernel!. Bit a bit y con tiempo se puede leer todo el contenido de la memoria del kernel aplicando esta operación millones de veces.

Notas finales

Los microprocesadores ARM1176, Cortex-A7, and Cortex-A53 usados en la Raspberry Pi no se ven afectados por el Meltdown ya que no poseen ejecución especulativa, los AMD Ryzen tampoco se ven afectados por el Meltdown ya que aunque si soportan ejecución especulativa al contrario de Intel la ejecución especulativa no se permite entre diferentes anillos de seguridad, el kernel se ejecuta en el anillo 0 y las aplicaciones en el anillo 3. Sin embargo, una variante de Meltdown es Spectre que es el mismo caso pero en vez de con la memoria del kernel con la memoria de otra aplicación. Como las aplicaciones se ejecutan en el mismo anillo en este caso los AMD Ryzen y algunos modelos de ARM si se ven afectados por Spectre al igual que también los Intel.

La ejecución especulativa hace más rápidos los microprocesadores pero habiéndose descubierto este fallo muy inseguros en el caso de los Intel ya que se ve afectados por Meltdown y Spectre a menos que se implementen parches por software ya que por microcódigo no es posible darle solución. Meltdown es más grave pero se puede corregir modificando el kernel aún con una pérdida de rendimiento, Spectre es más difícil de explotar pero más difícil de corregir y lo que se hará en este último caso es mitigar el problema modificando el sistema operativo, compilador y aplicaciones.

Intel tiene un problema importante, con AMD y sus Ryzen a buen precio, con buen rendimiento y… sin el problema del Meltdown. Para corregir el fallo en el diseño de la arquitectura del hardware Intel va a tener que rediseñar en parte su arquitectura y esto le va a llevar meses hasta tener preparados nuevos modelos de microprocesadores sin el error.

En el kernel de Linux 4.14.11 ya se han aplicado varios parches al igual que posteriormente se implementarán en Windows y macOS. Yo como usuario de Linux con un Intel Core i5-3210M que posee la característica pcid y esa versión del kernel no he notado ninguna perdida de rendimiento apreciable.


Comparte el artículo: