Add cache coherency

This commit is contained in:
augustin64 2024-08-15 19:43:47 +02:00
parent 370af6ee59
commit 2f7790f2bc

165
main.tex
View File

@ -26,7 +26,7 @@
Laboratoire \textsc{Irisa}\\
Université de \textsc{Rennes} 1}
\date{3 Juin 2024 - 12 Juillet 2024}
\date{3 juin 2024 - 12 juillet 2024}
% quelques macros
\newcommand{\TODO}[1]{{\color{red}#1}}
@ -59,10 +59,10 @@ Université de \textsc{Rennes} 1}
La mémoire DRAM d'un ordinateur est lente comparée à la fréquence du CPU. Le CPU dispose donc de caches,
basés sur une mémoire SRAM, plus petite mais plus rapide. Stocker en cache les éléments accédés
le plus fréquemment permet donc de réduire le nomre d'appels à la mémoire et donc le temps d'exécution.
le plus fréquemment permet donc de réduire le nombre d'appels à la mémoire et donc le temps d'exécution.
Les processeurs que nous étudions disposent de 3 niveaux de cache : L1, L2, L3.
Chaque processeur possède, au premier niveau, un L1i (cache des instructions) et
Chaque coeur possède, au premier niveau, un L1i (cache des instructions) et
un L1d (cache des données). Au second niveau, il possède un L2 qui contient potentiellement
des données et des instructions. Au dernier niveau, le L3 est partagé entre tous les coeurs de la
\ang{socket}, et celui-ci est inclusif de tous les caches de niveau inférieur : toute ligne
@ -70,12 +70,12 @@ stockée dans le cache L1 ou L2 est également dans le L3.
Si le L3 est partagé, il n'est cependant pas situé en un seul endroit dans le CPU
mais est réparti en différentes \ang{slices} : des tranches de mémoire accolées chacune à un coeur. Dans le modèle
étudié, chaque coeur a exactement une \ang{slice}\footnote{\TODO{c'est plus compliqué sur les nouveaux proc}}.
étudié, chaque coeur a exactement une \ang{slice}.
\begin{figure}[ht]
\centering
\includegraphics[width=0.4\textwidth]{broadwell-die-shot}
\caption{Broadwell Deca-Core die shot by Intel - annotated by Wikichip \cite{broadwelldieshot}}
\caption{Broadwell Deca-Core die shot by Intel - annotated by Wikichip~\cite{broadwelldieshot}}
% L'espace vide l'est bien sur le die-shot, donc sûrement "Intel being stupid"
\end{figure}
@ -83,25 +83,70 @@ Lorsqu'un coeur accède à une donnée qui n'est pas encore dans son cache, c'es
les 64 octets environnants (généralement) qui sont chargés dans son L1 ou L2. Comme le L3 est inclusif,
la ligne y est chargée également. Une fonction de hachage non-documentée attribue à chaque adresse
physique une unique \ang{slice} dans laquelle elle peut être mise en cache.
Différents travaux\cite{slice-reverse, practicalTiming} ont permis de déterminer cette fonction.
Différents travaux~\cite{slice-reverse, practicalTiming} ont permis de déterminer cette fonction.
\subsection{\TODO{Protocoles de cohérence de cache}}
\subsection{Protocoles de cohérence de cache}
\TODO{
\begin{enumerate}
\item Problème à résoudre
\item Expliquer la solution par "directory"
\item Expliquer le snooping
\end{enumerate}
}
Dans des systèmes à plusieurs coeurs, d'autant plus avec plusieurs processeurs, où chaque coeur a
un cache qui lui est propre, un problème de cohérence apparaît. Comment s'assurer qu'une ligne de données
ne soit pas réécrite de différentes manières en plusieurs cache du système ?
On peut commencer par définir des états dans lesquels
sont considérées les lignes de cache :
\begin{itemize}
\item \emph{M} Modifié : la ligne est stockée modifiée dans un unique cache
\item \emph{E} Exclusif : la ligne est stockée intacte dans un unique cache
\item \emph{S} Partagé : la ligne est stockée intacte dans plusieurs caches
(plusieurs caches disjoints, des L1 de coeurs différents par exemple)
\item \emph{I} Invalide : la ligne a été invalidée dans ce cache,
car modifiée dans un autre cache par exemple
\end{itemize}
D'autres sont parfois ajoutés à cette liste comme \ang{Forward}.
Une première solution au problème de cohérence de cache,
dite par annuaire (\ang{directory}), consiste à avoir à côté des différents
caches un \ang{directory} qui contient pour chaque ligne de mémoire en cache
son état dans les différents endroits où elle est stockée.
Lorsqu'une donnée partagée est modifiée, le \ang{directory} est chargé d'envoyer
aux autres caches une requête invalidant la ligne modifiée
Dans l'autre solution principalement utilisée, dite par \ang{snooping},
chaque cache surveille de son côté les lignes qu'il a en mémoire.
Dans le système étudié, la cohérence se gère par \ang{directory} au sein d'une
même \ang{socket}, mais par \ang{snooping} entre les deux \ang{sockets}.
\subsection{Attaques par canaux auxiliaires}
\TODO{Expliquer le concept général}
\subsubsection{Mémoire partagée}
\TODO{était bien formulé dans le 2.2 de~\cite{flushflush}
donc reprend largement les mêmes idées, ce n'est pas une citation et ces infos
ne sont pas non plus particulières à ce papier.
Comment dire que ça en est inspiré, faut-il le faire ?}
Les systèmes d'exploitation utilisent le principe de mémoire partagée pour
réduire l'utilisation totale de mémoire physique. C'est-à-dire que différentes
pages de mémoire virtuelle correspondent à une même page de mémoire physique,
partagée potentiellement entre plusieurs processus.
Par exemple, les bibliothèques utilisées par plusieurs programmes ne sont chargées
qu'une seule fois en mémoire pour tous les programmes les utilisant.
De la même manière, lorsque un processus est dupliqué (via \ang{fork})
ou lancé deux fois, les données qu'ils ont en commun
(le code du programme par exemple) sont partagées entre
les différentes instances du programme.
Une autre forme de déduplication consiste à regarder
les pages de mémoire contenant les mêmes données et à les combiner.
Cela peut amener différents processus \TODO{\ang{sandbox}és}
ou même de machines virtuelles différentes à partager des données en commun.
\subsubsection{L'instruction \texttt{clflush}}
D'après le manuel Intel\cite{intel-man-vol1}:
D'après le manuel Intel~\cite{intel-man-vol1}:
\begin{displayquote}
\sffamily \emph{
CLFLUSH (flush cache line) instruction writes and invalidates the cache line associated
@ -111,7 +156,7 @@ D'après le manuel Intel\cite{intel-man-vol1}:
\end{displayquote}
Lorsque l'instruction \texttt{clflush} est exécutée, l'adresse et la ligne de cache associée sont
évincés de tous les caches L1, L2 et L3 où elles se trouvaient possiblement, et cela dans tous les
évincées de tous les caches L1, L2 et L3 où elles se trouvaient possiblement, et cela dans tous les
\ang{sockets} du système. Si des modifications avaient eu lieu,
les modifications sont réécrites dans la mémoire DRAM.
@ -120,13 +165,11 @@ les adresses mémoires auxquelles il a accès.
\subsubsection{Flush+Reload}
\TODO{2.2 \& 2.3 of \cite{flushflush}}
Le temps de chargement d'une donnée est largement influencé par sa présence en cache.
Mesurer le temps de chargement d'une adresse permet donc de déterminer aisément si la ligne de
cache associée était déjà présente en cache.
Flush+Reload\cite{flushreload} propose donc la méthode suivante:
Flush+Reload~\cite{flushreload} propose donc la méthode suivante:
\begin{algorithm}[ht]
\caption{Flush+Reload}\label{alg:flushreload}
@ -150,7 +193,7 @@ et ne permet pas une haute fréquence d'observation.
Le temps d'exécution de l'instruction \texttt{clflush} dépendant de l'état de cohérence de la ligne
de cache concernée, la connaissance de son temps d'exécution permet de la même manière
de déterminer dans quel état était la ligne.
Flush+Flush\cite{flushflush} propose la méthode suivante :
Flush+Flush~\cite{flushflush} propose la méthode suivante :
\begin{algorithm}[ht]
\caption{Flush+Flush}\label{alg:flushflush}
@ -173,9 +216,6 @@ Les avantages de cette méthode par rapport à Flush+Reload sont multiples :
de données qui peuvent être extraites est bien plus élevé :
$496$KB/s contre $298$KB/s pour Flush+Reload
\TODO{what are packets in \cite{flushflush} ?
$\rightarrow$ j'ai eu une réponse il faut que je regarde}
\item Comme l'opération mesurée agit sur tous les caches du système et pas seulement sur ceux
utilisés par l'attaquant, Flush+Flush peut opérer dans un système avec une
hiérarchie de cache non inclusive. Ce qui est le cas des systèmes à deux
@ -184,10 +224,10 @@ Les avantages de cette méthode par rapport à Flush+Reload sont multiples :
\end{itemize}
Similairement à ces autres méthodes, Flush+Flush peut extraire des données du fonctionnement des
autres processus en regardant les accès mémoires faits dans les bibliothèques partagés,
autres processus en regardant les accès mémoires faits dans les bibliothèques partagées,
qui occupent les mêmes zones de la mémoire physique pour différents processus.
Daniel Gruss et al.\cite{cachetemplateattacks} proposent par exemple de récupérer
Daniel Gruss et al.~\cite{cachetemplateattacks} proposent par exemple de récupérer
le nonce d'une clé OpenSSL avec Flush+Reload
en regardant les zones mémoire accédées pendant le chiffrement de données.
Un enregistreur de frappe (\ang{keylogger}) basé sur
@ -198,12 +238,14 @@ les pages accédées dans la librairie \textsc{Gtk} \texttt{libgdk.so} y est ég
Là où Flush+Reload choisit de mesurer le temps pour charger à nouveau une adresse en mémoire,
Flush+Flush choisit de mesurer le temps nécessaire pour l'évincer : la différence entre
un \ang{cache hit} et un \ang{cache miss} est alors beaucoup moins perceptible (moins de 12 cycles de CPU).
De bons résultats\cite{flushflush} ont toutefois été obtenus en appliquant un seuil global.
De bons résultats~\cite{flushflush} ont toutefois été obtenus en appliquant un seuil global.
Guillaume \textsc{Didier} et Clémentine
\textsc{Maurice}~\cite{calibrationdoneright} proposent une rétro-ingénierie des
messages échangés en fonction de l'état de cohérence des lignes
évincées du cache. Cela passe par l'étude des sources de variabilité et permet de
mieux choisir un seuil propre à chaque combinaison attaquant/victime/\ang{slice}.
Guillaume \textsc{Didier} et Clémentine \textsc{Maurice}~\cite{calibrationdoneright}
proposent une autre approche : comprendre les messages échangés en fonction de l'état de cohérence
du cache ainsi que les autres sources de variabilité pour bien étalonner sur
l'ensemble des combinaisons pertinentes et choisir les bons seuils en fonction.
Ce travail s'était intéressé à certains processeurs Intel de micro-architectures \ang{Coffee Lake} et
\ang{Haswell} à une seule \ang{socket}, mais a révélé que les résultats seraient bien plus complexes
sur des systèmes à plusieurs \ang{sockets}.
@ -217,12 +259,12 @@ par processeur répondant aux critères suivants:
\item Processeur Intel ;
\item Exactement 2 \ang{socket} ;
\item Nombre de coeurs par \ang{socket} est une puissance de
deux \footnote{\TODO{l'expliquer en amont et le rappeler ici}} ;
deux \footnote{\TODO{l'expliquer en amont et le rappeler ici, voir Intro}} ;
\item Micro-architecture antérieure à SkyLake\footnote{Le L3 n'est plus inclusif
sur les processeurs serveur à partir de SkyLake}
sur les processeurs serveur à partir de SkyLake et la topologie devient plus complexe}
\end{itemize}
Les machines suivantes ont donc été utilisées\cite{g5k-nodes}
Les machines suivantes ont donc été utilisées~\cite{g5k-nodes}
\begin{center}
\begin{tabular}{|c||c|c|c|}
@ -246,7 +288,7 @@ Le \ang{NUMA balancing} a également été désactivé (à l'échelle du systèm
ce qui peut se faire pour le processus courant sans
privilèges\footnote{\TODO{à vérifier ! voir les "À faire"}}.
Les fichiers de résultats bruts sont accessibles en ligne\cite{g5k-results}.
Les fichiers de résultats bruts sont accessibles en ligne~\cite{g5k-results}.
\section{Analyse des résultats}
@ -254,7 +296,7 @@ Les fichiers de résultats bruts sont accessibles en ligne\cite{g5k-results}.
\begin{figure}[ht]
\centering
\includegraphics[width=0.5\textwidth]{low-core-count}
\caption{Topologie LCC Haswell EP d'après \cite{tuningXeon} \TODO{schéma à déplacer}}
\caption{Topologie LCC Haswell EP d'après~\cite{tuningXeon} \TODO{schéma à déplacer}}
\end{figure}
Les schémas de présentation d'Intel suggèrent une topologie en anneau, avec un CPU divisé en deux grandes parties.
@ -274,7 +316,7 @@ du QPI et du \ang{Home Agent}}:
\end{tabular}
\end{center}
Pour simplifier les interprétations, nous les avons renumérotés de la sorte,
Pour simplifier les interprétations, nous les avons renumérotés de la façon suivante,
c'est la numérotation que nous utiliserons pour la suite:
\begin{center}
@ -290,18 +332,17 @@ c'est la numérotation que nous utiliserons pour la suite:
Pour trouver la numérotation des \ang{slices}, deux méthodes sont possibles:
\begin{itemize}
\item Utiliser les compteurs de performance pour déterminer la \ang{slice} d'une adresse. Cela nécessite
\item Utiliser les compteurs de performance pour déterminer la \ang{slice} d'une adresse.
Cela nécessite
de lire les MSR correspondants donc d'avoir un accès \ang{root}. Nous avons suivi cette méthode
afin d'être sûr que les numéros de \ang{slice} correspondent aux numéros de coeurs ;
\item Utiliser la fonction de hachage (linéaire)\cite{slice-reverse} pour
déterminer la \ang{slice} de chaque adresse. Comme la
fonction est appliquée sur l'adresse virtuelle et non pas physique, l'ordre des \ang{slices} n'est pas le
bon mais deux adresses sont envoyées dans la même \ang{slice} uniquement si elles le sont réellement.
\TODO{clarifier, le côté math est mal expliqué}
Il serait envisageable de donner une méthode permettant de réordonner automatiquement les \ang{slices},
et donc d'exploiter ceci depuis un utilisateur non privilégié
\TODO{à réécrire aussi}
\item Utiliser la fonction de hachage~\cite{slice-reverse} pour
déterminer la \ang{slice} de chaque adresse. Comme la fonction est appliquée sur
l'adresse virtuelle et non pas physique, celle-ci est déterminée à permutation
près.\footnote{\TODO{référence à l'explication du nb de coeur = puissance de 2 en intro}}
Il serait envisageable de donner une méthode permettant de
réordonner automatiquement les \ang{slices}, et donc de déterminer l'information de la slice
depuis un utilisateur non privilégié.
\end{itemize}
@ -309,10 +350,7 @@ Pour trouver la numérotation des \ang{slices}, deux méthodes sont possibles:
\TODO{graphiques comparaison prédiction/réel}
\TODO{Je ne sais plus, en enlevant $S_R$,
est-ce que le modèle marchait bien avec socket(A) = socket(V) ?}
Les résultats obtenus quand le \ang{socket} attaquant et victime diffèrent suggèrent l'échange des messages suivant lors d'un \texttt{clflush} qui provoque un \ang{cache miss}:
Les résultats obtenus quand le \ang{socket} attaquant et victime diffèrent suggèrent l'échange des messages suivants lors d'un \texttt{clflush} qui provoque un \ang{cache miss}:
\begin{enumerate}
\item Le coeur attaquant contacte la \ang{slice} locale suivant \ref{figs:topology-miss}.
\item La \ang{slice} locale contacte la \ang{slice} distante en passant par le QPI.
@ -344,6 +382,11 @@ passer par lui.
\subsection{\TODO{Topologie Hit}}
\section{\TODO{Ouverture}}
\TODO{Extension au Xeon SP, L3 non inclusif+topologie différente
Extension aux nouveaux proc avec slice != 1/coeur}
\textbf{Aknowledgements} Experiments presented in this paper were carried out using the Grid'5000 testbed,
supported by a scientific interest group hosted by Inria and including \textsc{Cnrs},
\textsc{Renater} and several Universities as well as other organizations (see \url{https://www.grid5000.fr} ).
@ -354,16 +397,22 @@ supported by a scientific interest group hosted by Inria and including \textsc{C
\section{À faire pour continuer le stage}
\TODO{rédiger mieux qu'une liste}
Plusieurs pistes se proposent pour continuer le travail présenté dans ce rapport :
\begin{itemize}
\item vérifier que \texttt{numactl} permet de verrouiller les pages "partagées"
\item modèle hit : coût des sauts $A \rightarrow S$ cohérent avec $S_L \rightarrow S_R$ ?
\item \TODO{en plottant A=S=V pour toutes les valeurs possibles, ça ne montrait pas de preuve
du trajet \ang{slice}\_l-\ang{slice}\_r}
\item Clarifier les hypothèses avancées dans ce rapport :
sur la cohérence des modèles proposés avec la réalité et effectuer certaines vérifications :
\texttt{numactl} permet-il bien de verrouiller les pages partagées ? Peut-on proposer Une
renumérotation automatique des slices ?
\item proposer une méthode de réassignation automatique des slices pour savoir depuis un
utilisateur non privilégié la \ang{slice} d'une adresse virtuelle ;
\item réaliser l'attaque Flush+Flush sur des systèmes à 2 \ang{sockets}
\end{itemize}
\TODO{Faire un passage sur le code : qu'est-ce que j'ai apporté,
qu'est-ce qui était déjà fait. Où le trouver}
\TODO{Question à poser pour les systèmes à 10 sockets : si l'on désactive un coeur, la slice correspondante est-elle désactivée ? dans ce cas, qu'en est-il de la fonction de hachage ?}
\TODO{Faire un passage sur le code : ce que j'ai apporté,
ce qui était déjà fait. Où le trouver}
\TODO{Passage sur le labo, l'équipe, l'ambiance}