L'articolo citato da sgbj nei commenti scritti da Paul Turner di Google spiega quanto segue in modo molto più dettagliato, ma ci proverò:
Per quanto riesco a mettere insieme dalle informazioni limitate al momento, un retpoline è un trampolino di ritorno che utilizza un loop infinito che non viene mai eseguito per impedire alla CPU di speculare sull'obiettivo di un salto indiretto.
L'approccio di base può essere visto nel ramo del kernel di Andi Kleen che affronta questo problema:
Presenta la nuova __x86.indirect_thunk
chiamata che carica il target di chiamata il cui indirizzo di memoria (che chiamerò ADDR
) è memorizzato in cima allo stack ed esegue il salto usando RET
un'istruzione. Il thunk stesso viene quindi chiamato utilizzando la macro NOSPEC_JMP / CALL , utilizzata per sostituire molte chiamate e salti indiretti (se non tutti). La macro posiziona semplicemente la destinazione della chiamata nello stack e imposta correttamente l'indirizzo di ritorno, se necessario (notare il flusso di controllo non lineare):
.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
Il posizionamento di call
alla fine è necessario in modo che quando la chiamata indiretta è terminata, il flusso di controllo continua dietro l'uso della NOSPEC_CALL
macro, quindi può essere utilizzato al posto di un normalecall
Lo stesso thunk si presenta come segue:
call retpoline_call_target
2:
lfence /* stop speculation */
jmp 2b
retpoline_call_target:
lea 8(%rsp), %rsp
ret
Il flusso di controllo può creare un po 'di confusione qui, quindi vorrei chiarire:
call
spinge il puntatore dell'istruzione corrente (etichetta 2) nello stack.
lea
aggiunge 8 al puntatore dello stack , eliminando efficacemente il quadword inviato più di recente, che è l'ultimo indirizzo di ritorno (all'etichetta 2). Dopodiché, la parte superiore dello stack punta nuovamente all'indirizzo ADDR reale.
ret
passa *ADDR
e reimposta il puntatore dello stack all'inizio dello stack di chiamate.
Alla fine, tutto questo comportamento è praticamente equivalente a saltare direttamente a *ADDR
. L'unico vantaggio che otteniamo è che il predittore di diramazione utilizzato per le dichiarazioni di ritorno (Return Stack Buffer, RSB), quando esegue l' call
istruzione, presuppone che l' ret
istruzione corrispondente salterà all'etichetta 2.
La parte dopo l'etichetta 2 in realtà non viene mai eseguita, è semplicemente un ciclo infinito che teoricamente riempirebbe la pipeline di JMP
istruzioni con istruzioni. Utilizzando LFENCE
, PAUSE
o più in generale, un'istruzione che provoca lo stallo della pipeline di istruzioni, impedisce alla CPU di sprecare energia e tempo in questa esecuzione speculativa. Questo perché nel caso in cui la chiamata a retpoline_call_target ritornasse normalmente, LFENCE
sarebbe la prossima istruzione da eseguire. Questo è anche ciò che il predittore di filiale prevede in base all'indirizzo di ritorno originale (l'etichetta 2)
Per citare dal manuale di architettura Intel:
Le istruzioni che seguono un LFENCE possono essere recuperate dalla memoria prima di LFENCE, ma non verranno eseguite fino al completamento di LFENCE.
Si noti tuttavia che la specifica non menziona mai che LFENCE e PAUSE causano l'arresto della pipeline, quindi sto leggendo un po 'tra le righe qui.
Ora torniamo alla tua domanda originale: la divulgazione delle informazioni sulla memoria del kernel è possibile grazie alla combinazione di due idee:
Anche se l'esecuzione speculativa dovrebbe essere libera da effetti collaterali quando la speculazione era errata, l' esecuzione speculativa influisce comunque sulla gerarchia della cache . Ciò significa che quando un carico di memoria viene eseguito in modo speculativo, potrebbe aver causato l'eliminazione di una riga della cache. Questa modifica nella gerarchia della cache può essere identificata misurando attentamente il tempo di accesso alla memoria mappato sullo stesso set di cache.
È anche possibile perdere alcuni bit di memoria arbitraria quando l'indirizzo sorgente della memoria letto è stato letto dalla memoria del kernel.
Il predittore di diramazione indiretta delle CPU Intel utilizza solo i 12 bit più bassi dell'istruzione sorgente, quindi è facile avvelenare tutte le 2 ^ 12 possibili storie di previsione con indirizzi di memoria controllati dall'utente. Questi possono quindi, quando il salto indiretto è previsto nel kernel, essere speculativamente eseguiti con i privilegi del kernel. Utilizzando il canale laterale di temporizzazione della cache, è quindi possibile perdere la memoria del kernel arbitraria.
AGGIORNAMENTO: Nella mailing list del kernel , c'è una discussione in corso che mi porta a credere che le retpoline non mitigino completamente i problemi di previsione del ramo, come quando il Return Stack Buffer (RSB) gira vuoto, le architetture Intel più recenti (Skylake +) si ritirano al vulnerabile Branch Target Buffer (BTB):
Retpoline come strategia di mitigazione scambia i rami indiretti con i rendimenti, per evitare di usare previsioni che provengono dal BTB, in quanto possono essere avvelenate da un attaccante. Il problema con Skylake + è che un underflow RSB ricade sull'uso di una previsione BTB, che consente all'attaccante di assumere il controllo della speculazione.