Skip to content

Latest commit

 

History

History
404 lines (279 loc) · 19.4 KB

File metadata and controls

404 lines (279 loc) · 19.4 KB

🔝 Retour au Sommaire

45.4.2 — -D_FORTIFY_SOURCE

Section 45.4 — Compilation avec protections

Chapitre 45 — Sécurité en C++ ⭐


Introduction

_FORTIFY_SOURCE est un mécanisme de durcissement intégré à la glibc et exploité par GCC et Clang. Son principe : remplacer automatiquement, à la compilation, certaines fonctions C connues pour être dangereuses (memcpy, strcpy, sprintf, read, etc.) par des variantes fortifiées qui vérifient les tailles de tampon. Si le compilateur peut déterminer la taille du tampon de destination à la compilation, la variante fortifiée insère une vérification — statique ou dynamique — qui empêche l'écriture au-delà des bornes.

Le mécanisme est particulièrement pertinent en C++ car une part significative du code de production — surtout dans les couches basses (réseau, parsing, I/O) — utilise encore les fonctions C de la libc. Même dans du code qui fait un usage intensif de la STL, les appels à memcpy, memset ou snprintf restent fréquents dans les chemins critiques en performance. _FORTIFY_SOURCE ajoute une couche de protection sur ces appels sans modifier le code source.


Mécanisme détaillé

Comment fonctionne la fortification

Lorsque _FORTIFY_SOURCE est défini et que l'optimisation est activée (-O1 ou plus), le préprocesseur redirige les appels aux fonctions C dangereuses vers des wrappers définis dans les headers de la glibc. Ces wrappers utilisent le builtin __builtin_object_size() pour déterminer la taille du tampon de destination connue au moment de la compilation.

Trois cas se présentent :

Cas 1 — Le compilateur connaît les deux tailles. Si la taille de la source et la taille de la destination sont toutes deux connues à la compilation, le compilateur peut vérifier statiquement si l'opération est sûre. Si elle ne l'est pas, un warning (ou une erreur) est émis à la compilation — le bug est détecté avant même l'exécution :

#include <cstring>

void static_check() {
    char buf[8];
    std::memcpy(buf, "Hello, World!", 14);
    // Avec _FORTIFY_SOURCE : erreur ou warning à la compilation
    // "call to __memcpy_chk will always overflow destination buffer"
}

Cas 2 — Seule la taille de la destination est connue. Le compilateur connaît la taille du tampon de destination (par exemple, un tableau local) mais pas la taille de la copie (déterminée à l'exécution). Dans ce cas, la variante fortifiée insère une vérification dynamique qui compare la taille demandée à la taille du tampon au moment de l'exécution :

#include <cstring>
#include <cstddef>

void dynamic_check(const char* src, size_t len) {
    char buf[64];
    // Avec _FORTIFY_SOURCE : remplacé par __memcpy_chk(buf, src, len, 64)
    // Si len > 64 à l'exécution → abort immédiat
    std::memcpy(buf, src, len);
}

Le wrapper __memcpy_chk reçoit un quatrième paramètre : la taille connue du tampon de destination. Si la taille de la copie dépasse cette limite, __chk_fail est appelé et le programme est terminé.

Cas 3 — Taille de la destination inconnue. Si le compilateur ne peut pas déterminer la taille du tampon de destination (allocation dynamique via new ou malloc, pointeur passé en paramètre sans information de taille), la fortification ne peut pas s'appliquer. L'appel à la fonction originale est conservé tel quel, sans vérification :

#include <cstring>
#include <cstddef>

void no_check(char* dest, const char* src, size_t len) {
    // dest est un pointeur opaque — le compilateur ne connaît pas
    // la taille du tampon sous-jacent
    // _FORTIFY_SOURCE ne peut rien faire ici
    std::memcpy(dest, src, len);
}

C'est la limitation principale de _FORTIFY_SOURCE : il ne protège que les cas où le compilateur dispose d'une information de taille sur la destination. Les tableaux locaux et les tableaux à taille fixe sont bien couverts ; les allocations dynamiques le sont rarement.

Le rôle de __builtin_object_size

Le cœur du mécanisme repose sur le builtin du compilateur __builtin_object_size(ptr, type). Ce builtin retourne la taille de l'objet pointé par ptr si elle est connue à la compilation, ou (size_t)-1 si elle est inconnue.

Le paramètre type contrôle la précision de l'analyse :

Type Comportement Utilisé par
0 Taille maximale de l'objet entier _FORTIFY_SOURCE=1
1 Taille restante à partir du pointeur dans le sous-objet _FORTIFY_SOURCE=2
2 Comme 0, mais retourne 0 si inconnu (au lieu de -1) Interne
3 Comme 1, mais retourne 0 si inconnu _FORTIFY_SOURCE=3

La différence entre type 0 et type 1 est significative pour les accès à des membres de structures :

#include <cstring>

struct Packet {
    char header[4];
    char payload[64];
};

void copy_to_payload(Packet& pkt, const char* data, size_t len) {
    // Avec _FORTIFY_SOURCE=1 (type 0) :
    //   __builtin_object_size(&pkt.payload, 0) = sizeof(Packet) - offsetof(Packet, payload)
    //   = 68 octets — taille restante dans l'objet entier
    //   → accepte les copies jusqu'à 68 octets

    // Avec _FORTIFY_SOURCE=2 (type 1) :
    //   __builtin_object_size(&pkt.payload, 1) = sizeof(pkt.payload)
    //   = 64 octets — taille du sous-objet seul
    //   → rejette les copies > 64 octets, même si la mémoire existe après

    std::memcpy(pkt.payload, data, len);
}

_FORTIFY_SOURCE=2 est plus strict car il considère les bornes du champ individuel, pas de la structure entière. Un overflow de payload qui déborderait sur les octets suivants de la structure serait détecté avec le niveau 2 mais pas avec le niveau 1.


Les niveaux de protection

Niveau 1 : -D_FORTIFY_SOURCE=1

Le niveau de base. Utilise __builtin_object_size avec le type 0 (taille de l'objet entier). Les vérifications sont conservatrices : elles ne signalent un problème que si la copie dépasse la taille totale de l'objet contenant.

Ce niveau ne génère aucun code de vérification qui ne serait pas nécessaire — les fonctions dont la taille est connue et valide à la compilation sont remplacées par des appels directs optimisés. Le surcoût est essentiellement nul.

Niveau 2 : -D_FORTIFY_SOURCE=2 (recommandé) ⭐

Niveau plus strict. Utilise __builtin_object_size avec le type 1 (taille du sous-objet). Les vérifications sont plus fines et détectent les overflows internes aux structures (un champ qui déborde sur le suivant).

Le niveau 2 est le standard de l'industrie pour les binaires de production. C'est le défaut recommandé par les guides de sécurité de Debian, Ubuntu, Red Hat et Google. Le surcoût par rapport au niveau 1 est négligeable.

# Recommandé pour la production
g++ -O2 -D_FORTIFY_SOURCE=2 main.cpp -o main

Niveau 3 : -D_FORTIFY_SOURCE=3 (GCC 12+, Clang 15+)

Niveau le plus agressif, introduit récemment. Utilise __builtin_dynamic_object_size, une version améliorée de __builtin_object_size qui peut évaluer des tailles déterminées à l'exécution et pas seulement à la compilation. Cela étend la couverture aux cas où la taille du tampon est calculée dynamiquement mais reste traçable par le compilateur :

#include <cstdlib>
#include <cstring>

void level3_benefit(size_t n) {
    char* buf = (char*)malloc(n);  // Taille dynamique

    // Avec _FORTIFY_SOURCE=2 : pas de vérification possible
    //   (taille de buf inconnue à la compilation)
    
    // Avec _FORTIFY_SOURCE=3 : le compilateur peut propager la taille n
    //   via __builtin_dynamic_object_size et insérer une vérification
    //   dynamique dans certains cas

    std::memcpy(buf, "data", 5);
    free(buf);
}

Le niveau 3 est plus récent et son adoption en production est encore progressive. Il peut, dans de rares cas, générer du code légèrement moins performant que le niveau 2, car les vérifications dynamiques supplémentaires ont un coût non nul. Il est recommandé de le tester sur votre base de code avant de l'adopter en production.

Tableau comparatif

Niveau __builtin_object_size type Couverture Surcoût Support
1 0 (objet entier) Overflows dépassant l'objet Nul GCC 4.1+, Clang 5+
2 1 (sous-objet) Overflows internes aux structures Négligeable GCC 4.1+, Clang 5+
3 Dynamique Tailles dynamiques traçables Faible GCC 12+, Clang 15+

Fonctions couvertes par la fortification

_FORTIFY_SOURCE couvre un ensemble défini de fonctions de la glibc. Les principales catégories :

Manipulation de mémoire

Fonction originale Variante fortifiée Vérification
memcpy __memcpy_chk Taille de copie ≤ taille destination
memmove __memmove_chk Taille de copie ≤ taille destination
memset __memset_chk Taille de remplissage ≤ taille destination
mempcpy __mempcpy_chk Taille de copie ≤ taille destination

Manipulation de chaînes

Fonction originale Variante fortifiée Vérification
strcpy __strcpy_chk Longueur source + 1 ≤ taille destination
strncpy __strncpy_chk n ≤ taille destination
strcat __strcat_chk Longueur résultante ≤ taille destination
strncat __strncat_chk n + longueur existante ≤ taille destination

Formatage

Fonction originale Variante fortifiée Vérification
sprintf __sprintf_chk Résultat ≤ taille destination
snprintf __snprintf_chk n ≤ taille destination
vsprintf __vsprintf_chk Résultat ≤ taille destination
vsnprintf __vsnprintf_chk n ≤ taille destination

Pour sprintf en particulier, le niveau 2 ajoute également une vérification que le format ne contient pas %n lorsqu'il est utilisé avec un tampon sur la pile — une protection contre les format string attacks.

I/O système

Fonction originale Variante fortifiée Vérification
read __read_chk Taille de lecture ≤ taille destination
recv __recv_chk Taille de réception ≤ taille destination
recvfrom __recvfrom_chk Taille de réception ≤ taille destination
getcwd __getcwd_chk Taille ≤ taille destination

Fonctions non couvertes

_FORTIFY_SOURCE ne couvre pas toutes les fonctions potentiellement dangereuses. Parmi les absences notables :

  • std::copy et les algorithmes STL — ce sont des templates, pas des fonctions C libc. Elles ne sont pas interceptables par le mécanisme de fortification. Le hardening de la STL (section 45.1) couvre ce besoin.
  • new[]/delete[] — l'allocation C++ n'est pas couverte.
  • Fonctions custom — seules les fonctions de la glibc listées dans les headers fortifiés sont couvertes.

Prérequis et contraintes d'activation

_FORTIFY_SOURCE a un prérequis technique souvent méconnu : il nécessite une optimisation d'au moins -O1. Sans optimisation, le compilateur n'effectue pas les analyses de flux de données nécessaires pour déterminer les tailles de tampon, et __builtin_object_size retourne systématiquement (size_t)-1 (taille inconnue).

# ❌ _FORTIFY_SOURCE ignoré silencieusement avec -O0
g++ -O0 -D_FORTIFY_SOURCE=2 main.cpp -o main
# Warning : _FORTIFY_SOURCE requires compiling with optimization (-O)

# ✅ Fonctionnel avec -O1 ou plus
g++ -O2 -D_FORTIFY_SOURCE=2 main.cpp -o main

En mode debug (-O0), _FORTIFY_SOURCE n'a aucun effet. Ce n'est pas un problème en pratique car les builds de débogage utilisent les sanitizers (ASan, UBSan) qui offrent une couverture bien plus large. _FORTIFY_SOURCE cible spécifiquement les builds de production optimisés.

Autre contrainte : le niveau défini doit être cohérent dans toute l'unité de compilation. Définir _FORTIFY_SOURCE à des niveaux différents dans le même fichier (par exemple via des headers contradictoires) produit un warning et un comportement non défini du mécanisme. En pratique, le flag doit être défini une seule fois, globalement, via la ligne de compilation.

# ❌ Conflit de niveaux
g++ -D_FORTIFY_SOURCE=1 -D_FORTIFY_SOURCE=2 main.cpp
# Warning : _FORTIFY_SOURCE redefined

# ✅ Définition unique et globale
g++ -D_FORTIFY_SOURCE=2 main.cpp

Observer la fortification en action

Détection statique à la compilation

Quand le compilateur peut prouver un overflow à la compilation, il émet un warning — ou une erreur avec -Werror :

// overflow_static.cpp
#include <cstring>

void overflow_demo() {
    char buf[8];
    std::strcpy(buf, "This string is way too long for the buffer");
}
$ g++ -O2 -D_FORTIFY_SOURCE=2 -c overflow_static.cpp
In function 'char* strcpy(char*, const char*)',
    inlined from 'void overflow_demo()' at overflow_static.cpp:5:17:
/usr/include/x86_64-linux-gnu/bits/string_fortified.h:29:33: warning:
    '__builtin___strcpy_chk' writing 44 bytes into a region of size 8
    overflows the destination [-Wstringop-overflow=]

Le warning est précis : il indique la taille de l'écriture (44 octets), la taille du tampon (8 octets), et le site du problème. Ce type de détection statique est gratuit en performance — le bug est trouvé à la compilation.

Détection dynamique à l'exécution

Quand la taille n'est connue qu'à l'exécution, la vérification dynamique produit un abort immédiat :

// overflow_dynamic.cpp
#include <cstring>
#include <cstddef>

void copy_data(const char* src, size_t len) {
    char buf[32];
    std::memcpy(buf, src, len);  // Fortifié → __memcpy_chk(buf, src, len, 32)
}

int main() {
    char large[128];
    std::memset(large, 'A', sizeof(large));
    copy_data(large, sizeof(large));  // len=128 > buf size=32
    return 0;
}
$ g++ -O2 -D_FORTIFY_SOURCE=2 overflow_dynamic.cpp -o overflow_dynamic
$ ./overflow_dynamic
*** buffer overflow detected ***: terminated
Aborted (core dumped)

Le programme est terminé par __chk_fail avant que la copie excédentaire ne soit effectuée. L'overflow est détecté et neutralisé.

Vérifier la couverture sur un binaire

L'outil checksec (voir section 45.4) affiche deux métriques pour _FORTIFY_SOURCE :

  • Fortified — nombre de sites d'appel effectivement remplacés par des variantes fortifiées.
  • Fortifiable — nombre total de sites d'appel éligibles (fonctions couvertes avec taille de destination potentiellement déductible).
$ checksec --file=./my_program
...  Fortify  Fortified  Fortifiable
...  Yes      12         15

Un ratio Fortified/Fortifiable inférieur à 100 % est normal — il reflète les cas (cas 3 ci-dessus) où le compilateur ne dispose pas de l'information de taille nécessaire. Pour améliorer ce ratio, on peut refactoriser le code pour utiliser des tableaux locaux à taille fixe plutôt que des pointeurs opaques — mais dans la plupart des cas, la bonne solution est d'utiliser les conteneurs STL du C++ moderne, qui rendent _FORTIFY_SOURCE moins nécessaire.

On peut aussi inspecter les symboles du binaire pour vérifier la présence des variantes fortifiées :

# Lister les fonctions fortifiées utilisées
nm -D ./my_program | grep "_chk"

Sortie typique :

                 U __memcpy_chk@GLIBC_2.3.4
                 U __snprintf_chk@GLIBC_2.3.4
                 U __strcpy_chk@GLIBC_2.3.4

La présence de symboles *_chk confirme que la fortification est active pour ces fonctions.


Interaction avec le code C++ moderne

Ce que _FORTIFY_SOURCE protège dans du code C++

En C++ moderne bien écrit, l'essentiel des manipulations de données passe par les conteneurs STL et les algorithmes standards, qui ne sont pas couverts par _FORTIFY_SOURCE. Cependant, plusieurs situations justifient son activation même dans du C++ moderne :

Code legacy en cours de migration. Un projet en transition vers le C++ moderne contient souvent un mélange de fonctions C et de code STL. _FORTIFY_SOURCE protège la partie legacy pendant la migration.

Bibliothèques C liées au projet. De nombreuses dépendances (zlib, libpng, OpenSSL, SQLite, etc.) sont écrites en C et font un usage intensif des fonctions fortifiables. _FORTIFY_SOURCE les protège automatiquement si elles sont compilées avec le flag.

Appels memcpy/memset dans du code de performance. Même dans du code C++ moderne, il arrive que memcpy soit utilisé directement pour des copies de blocs — sérialisation, protocoles binaires, interfaçage avec du matériel. Ces appels bénéficient de la fortification.

Implémentation interne de la STL. Les implémentations de std::vector::resize, std::string::assign et d'autres méthodes utilisent en interne memcpy ou memmove. Si la libc est compilée avec _FORTIFY_SOURCE, ces appels internes peuvent bénéficier de la fortification — c'est une protection en profondeur.

Complémentarité avec le hardening STL

_FORTIFY_SOURCE et le hardening de la STL (voir section 45.1) sont complémentaires, pas redondants :

Mécanisme Protège Ne protège pas
_FORTIFY_SOURCE Appels libc (memcpy, strcpy, sprintf, read, ...) Conteneurs STL, itérateurs, operator[]
Hardening STL (libc++) operator[], itérateurs, préconditions STL Appels libc directs
Hardening STL (libstdc++) Préconditions, assertions Appels libc directs

Activer les deux simultanément maximise la couverture.


Intégration CMake

# Définir _FORTIFY_SOURCE globalement pour toutes les cibles
add_compile_definitions(_FORTIFY_SOURCE=2)

# S'assurer que l'optimisation est activée (prérequis)
# En CMake, les build types Release et RelWithDebInfo utilisent -O2 par défaut
# Le build type Debug utilise -O0 → _FORTIFY_SOURCE sera inactif (normal)

# Pour vérifier explicitement :
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    message(STATUS "_FORTIFY_SOURCE inactif en mode Debug (nécessite -O1+)")
endif()

Attention au conflit de définitions : certaines distributions (dont Ubuntu) définissent _FORTIFY_SOURCE=2 par défaut dans les specs du compilateur. Redéfinir la macro via CMake peut provoquer un warning de redéfinition. Pour l'éviter :

# Supprimer une éventuelle définition existante avant de redéfinir
add_compile_options(-U_FORTIFY_SOURCE)  
add_compile_definitions(_FORTIFY_SOURCE=2)  

Résumé

_FORTIFY_SOURCE est une protection à coût quasi nul qui transforme automatiquement les appels aux fonctions C dangereuses en variantes vérifiées. En résumé :

  • Utiliser le niveau 2 (-D_FORTIFY_SOURCE=2) comme défaut de production — c'est le standard de l'industrie, avec le meilleur rapport couverture/compatibilité.
  • Le niveau 3 est disponible sur GCC 12+ et Clang 15+ pour une couverture étendue aux tailles dynamiques — à évaluer au cas par cas.
  • L'optimisation -O1 minimum est requise — sans elle, le mécanisme est silencieusement inactif.
  • La couverture est limitée aux fonctions de la glibc — les conteneurs et algorithmes STL ne sont pas couverts. Combiner avec le hardening de la STL pour une protection complète.
  • Les vérifications sont à la fois statiques et dynamiques — certains overflows sont détectés dès la compilation, les autres à l'exécution par un abort immédiat.
  • Vérifier l'activation avec checksec (colonne Fortified/Fortifiable) ou nm -D | grep _chk.

⏭️ ASLR et PIE