El artículo mencionado por sgbj en los comentarios escritos por Paul Turner de Google explica lo siguiente con mucho más detalle, pero lo intentaré:
Hasta donde puedo reconstruir esto a partir de la información limitada en este momento, una retpoline es un trampolín de retorno que utiliza un bucle infinito que nunca se ejecuta para evitar que la CPU especule sobre el objetivo de un salto indirecto.
El enfoque básico se puede ver en la rama del núcleo de Andi Kleen que aborda este problema:
Introduce la nueva __x86.indirect_thunk
llamada que carga el destino de la llamada cuya dirección de memoria (que llamaré ADDR
) se almacena en la parte superior de la pila y ejecuta el salto usando la RET
instrucción. El thunk en sí mismo se llama utilizando la macro NOSPEC_JMP / CALL , que se utilizó para reemplazar muchas (si no todas) las llamadas indirectas y saltos. La macro simplemente coloca el destino de la llamada en la pila y establece la dirección de retorno correctamente, si es necesario (observe el flujo de control no lineal):
.macro NOSPEC_CALL target
jmp 1221f /* jumps to the end of the macro */
1222:
push \target /* pushes ADDR to the stack */
jmp __x86.indirect_thunk /* executes the indirect jump */
1221:
call 1222b /* pushes the return address to the stack */
.endm
La colocación de call
al final es necesaria para que cuando finalice la llamada indirecta, el flujo de control continúe detrás del uso de la NOSPEC_CALL
macro, por lo que se puede usar en lugar de un comando regular.call
El thunk en sí se ve de la siguiente manera:
call retpoline_call_target
2:
lfence /* stop speculation */
jmp 2b
retpoline_call_target:
lea 8(%rsp), %rsp
ret
El flujo de control puede ser un poco confuso aquí, así que déjenme aclarar:
call
empuja el puntero de instrucción actual (etiqueta 2) a la pila.
lea
agrega 8 al puntero de la pila , descartando efectivamente la última palabra cuádruple, que es la última dirección de retorno (a la etiqueta 2). Después de esto, la parte superior de la pila apunta a la dirección de retorno real ADDR nuevamente.
ret
salta *ADDR
y restablece el puntero de la pila al comienzo de la pila de llamadas.
Al final, todo este comportamiento es prácticamente equivalente a saltar directamente a *ADDR
. El único beneficio que obtenemos es que el predictor de rama utilizado para las declaraciones de retorno (Return Stack Buffer, RSB), al ejecutar la call
instrucción, supone que la ret
declaración correspondiente saltará a la etiqueta 2.
La parte posterior a la etiqueta 2 en realidad nunca se ejecuta, es simplemente un bucle infinito que en teoría llenaría la tubería de JMP
instrucciones con instrucciones. Al usar LFENCE
, PAUSE
o más generalmente, una instrucción que hace que el canal de instrucciones se detenga, la CPU no desperdicia energía y tiempo en esta ejecución especulativa. Esto se debe a que en caso de que la llamada a retpoline_call_target regrese normalmente, LFENCE
sería la siguiente instrucción que se ejecutará. Esto también es lo que predecirá el predictor de sucursal en función de la dirección de retorno original (la etiqueta 2)
Para citar del manual de arquitectura de Intel:
Las instrucciones que siguen a un LFENCE pueden recuperarse de la memoria antes del LFENCE, pero no se ejecutarán hasta que el LFENCE se complete.
Sin embargo, tenga en cuenta que la especificación nunca menciona que LFENCE y PAUSE hacen que la tubería se detenga, por lo que estoy leyendo un poco entre líneas aquí.
Ahora volvamos a su pregunta original: la divulgación de información de la memoria del núcleo es posible debido a la combinación de dos ideas:
Aunque la ejecución especulativa debería estar libre de efectos secundarios cuando la especulación era incorrecta, la ejecución especulativa todavía afecta la jerarquía de caché . Esto significa que cuando una carga de memoria se ejecuta especulativamente, aún puede haber provocado el desalojo de una línea de caché. Este cambio en la jerarquía de caché se puede identificar midiendo cuidadosamente el tiempo de acceso a la memoria que se asigna al mismo conjunto de caché.
Incluso puede perder algunos bits de memoria arbitraria cuando la dirección de origen de la lectura de la memoria fue leída de la memoria del núcleo.
El predictor indirecto de ramificación de las CPU Intel solo usa los 12 bits más bajos de la instrucción fuente, por lo que es fácil envenenar todos los 2 ^ 12 posibles historiales de predicción con direcciones de memoria controladas por el usuario. Estos pueden, entonces, cuando se predice el salto indirecto dentro del núcleo, ejecutarse especulativamente con privilegios de núcleo. Usando el canal lateral de sincronización de caché, puede perder memoria arbitraria del núcleo.
ACTUALIZACIÓN: En la lista de correo del kernel , hay una discusión en curso que me lleva a creer que las retpolines no mitigan por completo los problemas de predicción de la rama, como cuando el Buffer de pila de retorno (RSB) se ejecuta vacío, las arquitecturas Intel más recientes (Skylake +) retroceden al búfer de destino de rama vulnerable (BTB)
Retpoline como estrategia de mitigación intercambia ramas indirectas por retornos, para evitar el uso de predicciones que provienen del BTB, ya que pueden ser envenenadas por un atacante. El problema con Skylake + es que un desbordamiento de RSB recurre al uso de una predicción BTB, que permite al atacante tomar el control de la especulación.