L'article mentionné par sgbj dans les commentaires écrits par Paul Turner de Google explique beaucoup plus en détail ce qui suit, mais je vais essayer:
Pour autant que je puisse reconstituer cela à partir des informations limitées pour le moment, une retpoline est un trampoline de retour qui utilise une boucle infinie qui n'est jamais exécutée pour empêcher le CPU de spéculer sur la cible d'un saut indirect.
L'approche de base peut être vue dans la branche du noyau d'Andi Kleen traitant de ce problème:
Il introduit le nouvel __x86.indirect_thunk
appel qui charge la cible d'appel dont l'adresse mémoire (que j'appellerai ADDR
) est stockée au-dessus de la pile et exécute le saut à l'aide d'une RET
instruction. Le thunk lui-même est ensuite appelé à l'aide de la macro NOSPEC_JMP / CALL , qui a été utilisée pour remplacer de nombreux (sinon tous) appels et sauts indirects. La macro place simplement la cible d'appel sur la pile et définit correctement l'adresse de retour, si nécessaire (notez le flux de contrôle non linéaire):
.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
Le placement de call
à la fin est nécessaire pour que lorsque l'appel indirect est terminé, le flux de contrôle continue derrière l'utilisation de la NOSPEC_CALL
macro, de sorte qu'il peut être utilisé à la place d'un réguliercall
Le thunk lui-même ressemble à ceci:
call retpoline_call_target
2:
lfence /* stop speculation */
jmp 2b
retpoline_call_target:
lea 8(%rsp), %rsp
ret
Le flux de contrôle peut devenir un peu déroutant ici, alors permettez-moi de clarifier:
call
pousse le pointeur d'instruction actuel (étiquette 2) vers la pile.
lea
ajoute 8 au pointeur de pile , éliminant efficacement le dernier mot-clé poussé, qui est la dernière adresse de retour (à l'étiquette 2). Après cela, le haut de la pile pointe à nouveau sur la véritable adresse de retour ADDR.
ret
saute *ADDR
et réinitialise le pointeur de pile au début de la pile d'appels.
En fin de compte, tout ce comportement équivaut pratiquement à sauter directement vers *ADDR
. Le seul avantage que nous obtenons est que le prédicteur de branche utilisé pour les instructions de retour (Return Stack Buffer, RSB), lors de l'exécution de l' call
instruction, suppose que l' ret
instruction correspondante passera à l'étiquette 2.
La partie après le label 2 n'est jamais exécutée, c'est simplement une boucle infinie qui, en théorie, remplirait le pipeline d' JMP
instructions avec des instructions. En utilisant LFENCE
,PAUSE
ou plus généralement, une instruction entraînant le blocage du pipeline d'instructions, le processeur ne perd ni temps ni énergie pour cette exécution spéculative. En effet, au cas où l'appel à retpoline_call_target reviendrait normalement, ce LFENCE
serait la prochaine instruction à exécuter. C'est également ce que le prédicteur de branche prédira en fonction de l'adresse de retour d'origine (l'étiquette 2)
Pour citer le manuel d'architecture d'Intel:
Les instructions qui suivent un LFENCE peuvent être extraites de la mémoire avant le LFENCE, mais elles ne s'exécuteront pas tant que le LFENCE ne sera pas terminé.
Notez cependant que la spécification ne mentionne jamais que LFENCE et PAUSE provoquent le blocage du pipeline, donc je lis un peu entre les lignes ici.
Revenons maintenant à votre question initiale: la divulgation d'informations sur la mémoire du noyau est possible en raison de la combinaison de deux idées:
Même si l'exécution spéculative doit être sans effet secondaire lorsque la spéculation est erronée, l' exécution spéculative affecte toujours la hiérarchie du cache . Cela signifie que lorsqu'un chargement de mémoire est exécuté de manière spéculative, il peut toujours avoir provoqué l'expulsion d'une ligne de cache. Ce changement dans la hiérarchie du cache peut être identifié en mesurant soigneusement le temps d'accès à la mémoire qui est mappée sur le même ensemble de cache.
Vous pouvez même divulguer quelques bits de mémoire arbitraire lorsque l'adresse source de la mémoire lue a elle-même été lue dans la mémoire du noyau.
Le prédicteur de branche indirecte des processeurs Intel utilise uniquement les 12 bits les plus bas de l'instruction source, il est donc facile d'empoisonner tous les 2 ^ 12 historiques de prédiction possibles avec des adresses mémoire contrôlées par l'utilisateur. Ceux-ci peuvent alors, lorsque le saut indirect est prédit dans le noyau, être exécutés de manière spéculative avec les privilèges du noyau. En utilisant le canal latéral de synchronisation du cache, vous pouvez ainsi fuir la mémoire du noyau arbitraire.
MISE À JOUR: Sur la liste de diffusion du noyau , il y a une discussion en cours qui m'amène à penser que les retpolines n'atténuent pas complètement les problèmes de prédiction de branche, comme lorsque le Return Stack Buffer (RSB) est vide, les architectures Intel plus récentes (Skylake +) retombent à la cible vulnérable Branch Buffer (BTB):
La retpoline en tant que stratégie d'atténuation échange les branches indirectes contre des retours, pour éviter d'utiliser des prédictions qui proviennent du BTB, car elles peuvent être empoisonnées par un attaquant. Le problème avec Skylake + est qu'un sous-dépassement RSB revient à utiliser une prédiction BTB, ce qui permet à l'attaquant de prendre le contrôle de la spéculation.