Le modèle simplifié de Fil-C
(corsix.org)- Une architecture qui suit les pointeurs C/C++ en leur associant des métadonnées AllocationRecord, et effectue une vérification des bornes mémoire lors du déréférencement
- Une méthode qui fait circuler ensemble la valeur d’origine du pointeur et les métadonnées associées, ou qui convertit les appels en appels spécifiques à Fil-C, depuis l’affectation de pointeurs, l’arithmétique, le passage d’arguments aux fonctions, les retours, jusqu’aux appels à
mallocetfree - Les métadonnées des pointeurs dans la mémoire du tas sont stockées séparément dans invisible_bytes ; lors des chargements/stockages de pointeurs, la valeur et les métadonnées sont lues et écrites ensemble, avec en plus une vérification d’alignement
filc_freene libère quevisible_bytesetinvisible_bytesen conservant l’AllocationRecord lui-même ; le nettoyage ultérieur est pris en charge par le ramasse-miettes, et les variables locales dont l’adresse peut s’échapper sont promues sur le tas- Même si des complexités d’implémentation réelles subsistent, comme les threads, les pointeurs de fonction ou les optimisations mémoire et performances, cela peut servir d’exemple de système concret pour la vérification de la sûreté mémoire de grands codes C/C++ ou pour la pointer provenance
Modèle simplifié de Fil-C
- Fil-C utilise une architecture qui suit des métadonnées AllocationRecord* avec les pointeurs afin de traiter du code C/C++ de manière sûre en mémoire
- L’implémentation réelle repose sur une réécriture de l’IR LLVM, mais le modèle simplifié prend la forme d’une transformation automatique du code source C/C++
- Pour chaque variable locale de type pointeur dans une fonction, une variable locale
AllocationRecord*correspondante est ajoutée - Par exemple, pour
T1* p1, on ajouteAllocationRecord* p1ar = NULL
- Les affectations simples et calculs sur les variables locales pointeurs déplacent aussi AllocationRecord* avec la valeur d’origine du pointeur
p1 = p2est transformé enp1 = p2, p1ar = p2arp1 = p2 + 10s’accompagne aussi dep1ar = p2ar- Un cast d’un entier vers un pointeur définit les métadonnées à
NULL - Un cast d’un pointeur vers un entier reste inchangé
- Lors du passage d’arguments aux fonctions et des retours, AllocationRecord* est transmis en plus avec le pointeur, et certains appels de bibliothèque standard sont remplacés par des fonctions spécifiques à Fil-C
- Les appels à
mallocetfreesont transformés respectivement enfilc_mallocetfilc_free - Par exemple,
p1 = malloc(x); free(p1);devient{p1, p1ar} = filc_malloc(x); filc_free(p1, p1ar);
- Les appels à
filc_mallocn’alloue pas seulement la mémoire demandée, mais effectue trois allocations- Allocation d’un objet
AllocationRecord - Allocation de
visible_bytespour les données réelles - Allocation par
callocdeinvisible_bytespour stocker les métadonnées invisibles AllocationRecordcontient les champsvisible_bytes,invisible_bytesetlength
- Allocation d’un objet
Déréférencement et vérification des bornes
- Lors du déréférencement d’un pointeur, une vérification des bornes est effectuée à l’aide de l’AllocationRecord* associé
- Vérifier que les métadonnées du pointeur ne sont pas
NULL - Calculer la différence entre la position courante du pointeur et l’adresse de début de
visible_bytes - Vérifier que l’offset est inférieur à la longueur totale
- Vérifier que la longueur restante est suffisante pour la taille de la cible du déréférencement
- Vérifier que les métadonnées du pointeur ne sont pas
- La même procédure de vérification s’applique à la lecture comme à l’écriture
- Une vérification est effectuée avant
x = *p1 - La même forme de vérification est effectuée avant
*p2 = x
- Une vérification est effectuée avant
- Cette structure empêche les accès où la cible pointée sort de la plage allouée
Pointeurs dans le tas et invisible_bytes
- Les pointeurs stockés dans la mémoire du tas ne peuvent pas être gérés par le compilateur via des variables séparées comme les variables locales ; invisible_bytes est donc utilisé
- S’il y a un pointeur à la position
visible_bytes + i, l’AllocationRecord*correspondant est stocké à la positioninvisible_bytes + i - Autrement dit,
invisible_bytesse comporte comme un tableau dont le type d’élément estAllocationRecord*
- S’il y a un pointeur à la position
- Lorsqu’une valeur de pointeur est lue ou écrite en mémoire, une vérification d’alignement s’ajoute à la vérification de bornes habituelle
- Vérifier que l’offset
iest un multiple desizeof(AllocationRecord*) - Cette condition doit être satisfaite pour pouvoir accéder en toute sûreté à
invisible_bytescomme à un tableau deAllocationRecord**
- Vérifier que l’offset
- Lors d’un chargement de pointeur, les métadonnées sont chargées en même temps que le pointeur de données
p2 = *p1est suivi dep2ar = *(AllocationRecord**)(p1ar->invisible_bytes + i)
- Lors d’un stockage de pointeur, les métadonnées correspondantes sont écrites en plus de la valeur du pointeur
*p1 = p2effectue, après le stockage des données réelles,*(AllocationRecord**)(p1ar->invisible_bytes + i) = p2ar
filc_free et le ramasse-miettes
- Quand le pointeur n’est pas
NULL,filc_freevérifie la cohérence avec AllocationRecord puis ne libère que deux zones mémoire- Vérifier
par != NULL - Vérifier
p == par->visible_bytes - Libérer
visible_bytesetinvisible_bytes - Puis définir
visible_bytesetinvisible_bytesàNULL, etlengthà 0
- Vérifier
- Même si
filc_malloceffectue trois allocations,filc_freene libère pas l’objet AllocationRecord lui-même- Cette différence est gérée par le ramasse-miettes
- Dans le modèle simplifié, un GC stop-the-world suffit ; le vrai Fil-C utilise un collecteur parallèle, concurrent et incrémental
- Le GC suit les objets
AllocationRecord - Les
AllocationRecordinaccessibles sont marqués pour libération
- Le GC suit les objets
- Le GC effectue en plus deux tâches
- Appeler
filc_freelors de la libération d’unAllocationRecordinaccessible - Remplacer tous les pointeurs vers un
AllocationRecorddontlengthvaut 0 par un uniqueAllocationRecordcanonique de longueur 0
- Appeler
- Grâce à ce comportement, l’absence d’appel à
freene mène pas à une fuite mémoire- Le GC effectue la libération automatiquement
- Mais l’appel à
freepermet une libération plus précoce que celle du GC
- Après
free, l’AllocationRecordconcerné finit de toute façon par devenir inaccessible et peut être nettoyé plus tard
Échappement d’adresse des variables locales et promotion sur le tas
- En présence d’un GC, la plage dans laquelle l’adresse d’une variable locale peut être manipulée en toute sûreté s’élargit
- Si l’adresse d’une variable locale a été prise et que le compilateur ne peut pas prouver que cette adresse ne s’échappera pas au-delà de la durée de vie de la variable, elle est promue vers une allocation sur le tas
- Ces variables locales sont alors allouées via
mallocau lieu de la pile- Il n’est pas nécessaire d’insérer un
freecorrespondant - Le GC s’occupe de la récupération
- Il n’est pas nécessaire d’insérer un
Version Fil-C de memmove
- Le
memmovede la bibliothèque standard C traite une mémoire arbitraire, ce qui pose le problème suivant : le compilateur ne peut pas savoir s’il y a des pointeurs à l’intérieur - Pour cela, une heuristique est appliquée
- Les pointeurs dans une mémoire arbitraire doivent y être entièrement contenus dans cette plage mémoire
- Les pointeurs doivent être correctement alignés
- Cette règle entraîne des différences de comportement même pour le déplacement des mêmes 8 octets
- Si 8 octets alignés sont déplacés d’un seul coup par
memmove, la portion correspondante deinvisible_bytesse déplace aussi - Si
memmoveest exécuté 8 fois octet par octet,invisible_bytesne se déplace pas
- Si 8 octets alignés sont déplacés d’un seul coup par
Complexités supplémentaires dans l’implémentation réelle
-
Threads
- La concurrence augmente la complexité du GC
filc_freene peut pas libérer immédiatement la mémoire- Car il peut y avoir une condition de course entre le thread qui libère et un autre thread accédant à la même mémoire
- Les opérations atomiques sur les pointeurs nécessitent aussi un traitement supplémentaire
- La réécriture de base remplace les chargements/stockages de pointeurs par deux chargements/stockages, ce qui brise l’atomicité
-
Pointeurs de fonction
- Des métadonnées supplémentaires dans
AllocationRecordpermettent d’indiquer quevisible_bytesn’est pas une donnée ordinaire mais un pointeur vers du code exécutable - Un appel via un pointeur de fonction
p1vérifiep1 == p1ar->visible_byteset contrôle aussi quep1arreprésente bien un pointeur de fonction - Pour empêcher les attaques de confusion de type sur les pointeurs de fonction, une vérification de signature de type est aussi nécessaire dans l’ABI d’appel
- Une méthode consiste à donner la même signature de type à toutes les fonctions
- En traitant cela comme un passage en mémoire de tous les arguments regroupés dans une structure
- À la frontière ABI, chaque fonction ne reçoit alors qu’un seul
AllocationRecordcorrespondant à cette structure
- Des métadonnées supplémentaires dans
-
Optimisation de l’usage mémoire
- On peut envisager que
filc_mallocn’alloue pas immédiatementinvisible_bytes, mais le fasse à la demande - On peut aussi envisager de placer
AllocationRecordetvisible_bytesensemble dans une seule allocation - Si le
mallocsous-jacent attache des métadonnées en tête de chaque allocation, on peut aussi envisager d’intégrer ces métadonnées àAllocationRecord
- On peut envisager que
-
Optimisation des performances
- La sûreté mémoire de Fil-C a un coût en performances
- Il existe une marge pour appliquer diverses techniques afin de récupérer une partie des performances perdues
Quand utiliser Fil-C
- Fil-C peut être utilisé pour de grands codes C/C++ qui semblent fonctionner mais sans vérification de sûreté mémoire, lorsque l’on peut accepter l’introduction d’un GC et une forte baisse de performances au nom de la sûreté mémoire
- Il est mentionné comme mesure temporaire possible avant une réécriture en Java, Go ou Rust
- Comme ASan, Fil-C peut aussi être exécuté dans un but de détection de bugs mémoire
- Il est possible d’exécuter du code C/C++ sous Fil-C pour vérifier la présence de bugs mémoire
- Dans les langages dont le langage de compilation et le langage d’exécution sont identiques, et dont la sûreté à la compilation est forte, cela peut servir à une évaluation sûre à la compilation
- Zig est cité en exemple
- Même si l’évaluation à l’exécution n’est pas sûre, l’évaluation à la compilation peut utiliser une configuration de type Fil-C
- Cela a aussi un intérêt comme exemple de système concret traitant la pointer provenance
- La question est posée de savoir si, lorsque
p1etp2ont le même type, l’optimisation consistant à transformerif (p1 == p2) { f(p1); }enif (p1 == p2) { f(p2); }est possible - Dans Fil-C, comme les
AllocationRecord*transmis àfdiffèrent, il est explicitement indiqué que la réponse est clairement non - En ce sens, Fil-C sert d’exemple de système concret doté de pointer provenance
- La question est posée de savoir si, lorsque
Aucun commentaire pour le moment.