Der von sgbj in den Kommentaren von Paul Turner von Google erwähnte Artikel erklärt Folgendes viel ausführlicher, aber ich werde es versuchen:
Soweit ich dies aus den derzeit begrenzten Informationen zusammensetzen kann, ist eine Retpoline ein Rücklauftrampolin , das eine Endlosschleife verwendet, die niemals ausgeführt wird, um zu verhindern, dass die CPU über das Ziel eines indirekten Sprungs spekuliert.
Der grundlegende Ansatz ist in Andi Kleens Kernel-Zweig zu sehen, der sich mit diesem Problem befasst:
Es führt den neuen __x86.indirect_thunk
Aufruf ein, der das Aufrufziel lädt, dessen Speicheradresse (die ich aufrufen werde ADDR
) oben auf dem Stapel gespeichert ist, und führt den Sprung mithilfe der RET
Anweisung aus. Der Thunk selbst wird dann mit dem Makro NOSPEC_JMP / CALL aufgerufen, mit dem viele (wenn nicht alle) indirekte Aufrufe und Sprünge ersetzt wurden. Das Makro platziert einfach das Aufrufziel auf dem Stapel und stellt die Rücksprungadresse bei Bedarf korrekt ein (beachten Sie den nichtlinearen Kontrollfluss):
.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
Die Platzierung von call
am Ende ist erforderlich, damit der Kontrollfluss nach Beendigung des indirekten Aufrufs hinter der Verwendung des NOSPEC_CALL
Makros fortgesetzt wird , sodass er anstelle eines regulären Makros verwendet werden kanncall
Der Thunk selbst sieht wie folgt aus:
call retpoline_call_target
2:
lfence /* stop speculation */
jmp 2b
retpoline_call_target:
lea 8(%rsp), %rsp
ret
Der Kontrollfluss kann hier etwas verwirrend sein, lassen Sie mich Folgendes klarstellen:
call
schiebt den aktuellen Anweisungszeiger (Beschriftung 2) auf den Stapel.
lea
Fügt dem Stapelzeiger 8 hinzu und verwirft effektiv das zuletzt gepusste Quadword, das die letzte Rücksprungadresse ist (zu Bezeichnung 2). Danach zeigt die Oberseite des Stapels wieder auf die reale Rücksprungadresse ADDR.
ret
springt zum *ADDR
Stapelzeiger und setzt ihn auf den Anfang des Aufrufstapels zurück.
Am Ende ist dieses ganze Verhalten praktisch gleichbedeutend mit direktem Springen *ADDR
. Der einzige Vorteil, den wir erhalten, besteht darin, dass der für return-Anweisungen verwendete Verzweigungsprädiktor (Return Stack Buffer, RSB) bei der Ausführung des call
Befehls davon ausgeht, dass die entsprechende ret
Anweisung zum Label 2 springt.
Der Teil nach dem Label 2 wird tatsächlich nie ausgeführt, es ist einfach eine Endlosschleife, die theoretisch die Anweisungspipeline mit JMP
Anweisungen füllen würde . Durch die Verwendung von LFENCE
, PAUSE
oder allgemeiner eine Anweisung verursacht die Befehlspipeline Strömungsabriß sein stoppt die CPU verschwendet keine Energie und Zeit auf dieser spekulativen Ausführung. Dies liegt daran, dass für den Fall, dass der Aufruf von retpoline_call_target normal zurückkehren LFENCE
würde, dies die nächste auszuführende Anweisung wäre. Dies ist auch das, was der Verzweigungsprädiktor basierend auf der ursprünglichen Rücksprungadresse (dem Etikett 2) vorhersagt.
Um aus Intels Architekturhandbuch zu zitieren:
Anweisungen, die einem LFENCE folgen, können vor dem LFENCE aus dem Speicher abgerufen werden, werden jedoch erst ausgeführt, wenn der LFENCE abgeschlossen ist.
Beachten Sie jedoch, dass in der Spezifikation niemals erwähnt wird, dass LFENCE und PAUSE die Pipeline zum Stillstand bringen. Daher lese ich hier ein wenig zwischen den Zeilen.
Zurück zu Ihrer ursprünglichen Frage: Die Offenlegung von Kernelspeicherinformationen ist aufgrund der Kombination zweier Ideen möglich:
Obwohl die spekulative Ausführung nebenwirkungsfrei sein sollte, wenn die Spekulation falsch war, wirkt sich die spekulative Ausführung immer noch auf die Cache-Hierarchie aus . Dies bedeutet, dass bei einer spekulativen Ausführung eines Speicherladevorgangs möglicherweise immer noch eine Cache-Zeile entfernt wurde. Diese Änderung in der Cache-Hierarchie kann identifiziert werden, indem die Zugriffszeit auf den Speicher, der demselben Cache-Satz zugeordnet ist, sorgfältig gemessen wird.
Sie können sogar einige Bits beliebigen Speichers verlieren, wenn die Quelladresse des gelesenen Speichers selbst aus dem Kernelspeicher gelesen wurde.
Der indirekte Verzweigungsprädiktor von Intel-CPUs verwendet nur die untersten 12 Bits des Quellbefehls, so dass es einfach ist, alle 2 ^ 12 möglichen Vorhersageverläufe mit benutzergesteuerten Speicheradressen zu vergiften. Diese können dann, wenn der indirekte Sprung innerhalb des Kernels vorhergesagt wird, spekulativ mit Kernel-Berechtigungen ausgeführt werden. Mit dem Cache-Timing-Seitenkanal können Sie somit beliebigen Kernelspeicher verlieren.
UPDATE: Auf der Kernel-Mailingliste gibt es eine laufende Diskussion, die mich zu der Annahme führt, dass Retpolinen die Probleme mit der Verzweigungsvorhersage nicht vollständig mindern, da neuere Intel-Architekturen (Skylake +) zurückfallen, wenn der Return Stack Buffer (RSB) leer ist an den anfälligen Branch Target Buffer (BTB):
Retpoline als Minderungsstrategie tauscht indirekte Zweige gegen Renditen aus, um zu vermeiden, dass Vorhersagen verwendet werden, die vom BTB stammen, da sie von einem Angreifer vergiftet werden können. Das Problem bei Skylake + ist, dass ein RSB-Unterlauf auf die Verwendung einer BTB-Vorhersage zurückgreift, die es dem Angreifer ermöglicht, die Kontrolle über Spekulationen zu übernehmen.