August 8, 20255 min

Partie 2 : Les languages comme C/C++ ou Zig permettent de contrôler la mémoire mais à quel prix ?

m
mayo

Avec C, il n'y a pas de runtime, pas de GC.

char* msg = malloc(100);
strcpy(msg, "hello");
free(msg);
printf("%s", msg); // ❌ Use after free

Pièges Courants

Problème Code Risque
Use-after-free printf("%s", msg); Undefined behavior
Double free free(msg); free(msg); Heap corruption
Buffer overflow char buf[4]; strcpy(buf, "long"); Memory corruption
Memory leak malloc(...) sans free Crashes lents

Modèle de Mémoire Manuelle

Tu dois :

  • Allouer la mémoire
  • Tracker l'ownership
  • La libérer manuellement
  • Éviter d'accéder à la mémoire freed ou invalide

Conséquences Réelles

Voici quelques bugs connus.

Heartbleed (OpenSSL)

// Version simplifiée du bug
char* buffer = malloc(payload_length);
memcpy(buffer, payload, payload_length); // Pas de bounds check !
// L'attaquant pouvait lire au-delà du buffer

Impact : Plus de 500 000 serveurs ont exposé leurs clés privées et mots de passe.

CVE-2021-44228 (Équivalent Log4Shell en C)

char* user_input = get_user_data();
sprintf(log_buffer, "User: %s", user_input); // Buffer overflow possible

Le Problème : Pas de bounds checking automatique signifie que les attaquants peuvent :

  • Crasher ton programme
  • Exécuter du code arbitraire
  • Voler des données sensibles

Sécurité de la mémoire niveau statique (dans le code statique, avant l'exécution / runtime)

Vulnérabilités de sécurité par catégorie :

  • 70% des bugs sécurité Microsoft : problèmes de memory safety
  • 65% des vulnérabilités Chrome : memory corruption
  • ~50% des patches sécurité Android : liés à la mémoire

Un poid pour le développeur

Chaque allocation nécessite un tracking

typedef struct {
    char* data;
    size_t size;
} Buffer;

Buffer* create_buffer(size_t size) {
    Buffer* buf = malloc(sizeof(Buffer));
    if (!buf) return NULL;
    
    buf->data = malloc(size);
    if (!buf->data) {
        free(buf);  // Il faut se rappeler de nettoyer !
        return NULL;
    }
    
    buf->size = size;
    return buf;
}

void destroy_buffer(Buffer* buf) {
    if (buf) {
        free(buf->data);  // Il faut free dans le bon ordre
        free(buf);
    }
}

Saturation mental : Chaque fonction doit considérer :

  • Qui possède ce pointer ?
  • Quand doit-il être liberé ?
  • Est-il encore valide ?

Debugging des Problèmes Mémoire

$ valgrind ./my_program
==12345== Invalid read of size 4
==12345==    at 0x40084B: main (test.c:10)
==12345==  Address 0x5204044 is 0 bytes after a block of size 4 alloc'd
==12345==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)

Un des problèmes majeurs : Les bugs sont découverts trop tard, au runtime, pas au moment de la compilation.

Trade-off Performance vs Sécurité

Caractéristiques Performance de C

// Zero overhead - accès mémoire direct
int sum = 0;
for (int i = 0; i < 1000000; i++) {
    sum += array[i];  // Pas de bounds checking
}

Vitesse : ✅ Performance maximale
Sécurité : ❌ Une erreur = vulnérabilité de sécurité

Contrôle de l'empreinte mémoire

// Contrôle précis de l'emprunte mémoire
struct Point {
    float x, y, z;     // Exactement 12 bytes
} __attribute__((packed));

Point* points = malloc(1000 * sizeof(Point)); // Allocation prévisible

Contrôle : ✅ Contrôle complet de l'emprunte mémoire Risque : ❌ Gestion manuelle des lifetimes

Les Outils aident, mais ne suffisent pas

Static Analysis

// clang-static-analyzer peut attraper certains problèmes
char* ptr = malloc(10);
free(ptr);
*ptr = 'x';  // ⚠️ Warning: use after free

Runtime Detection

// AddressSanitizer (ASan) attrape les bugs au runtime
$ gcc -fsanitize=address program.c
$ ./a.out
=================================================================
==12345==ERROR: AddressSanitizer: heap-use-after-free

La Limitation

  • Outils static : Ratent les cas complexes, faux positifs
  • Outils runtime : Ne détectent les bugs qui s'exécutent que pendant les tests
  • Code review : Erreur humaine, chronophage

Pourquoi C est toujours utilisé malgré les risques

Exigences de programmation système

  • Systèmes d'exploitation : Besoin d'accès direct au hardware
  • Systèmes embarqués : Contraintes mémoire, pas de place pour un runtime
  • Code critique en performance : Chaque nanoseconde compte

Legacy et Écosystème

  • Bases de code massives : Décennies de code C en production
  • Écosystème de librairies : La plupart des librairies système écrites en C
  • Connaissance développeur : Générations de programmeurs C

Le Problème Fondamental

C te donne deux mauvais choix :

Option 1 : Gestion manuelle de la mémoire

char* data = malloc(size);
// ... logique complexe ...
if (error) {
    free(data);  // Il faut se souvenir du cleanup dans TOUS les chemins
    return -1;
}
// ... plus de logique ...
free(data);  // Facile d'oublier ou de double-free

Option 2 : Garbage collector

  • Ajouter une librairie GC comme Boehm GC
  • Perdre la prévisibilité des performances
  • Toujours possible d'avoir des fuites mémoires

Points Clés

Performance prévisible - pas de pauses GC
Contrôle complet de l'emprunte mémoire
Overhead runtime minimal
Unsafe par défaut - une erreur = vulnérabilité
Responsabilité élevé pour les développeurs
La plupart des bugs sécurité viennent des problèmes mémoire
Les outils détectent les bugs après qu'ils soient écrits, pas avant


Le Défi : Nous voulons la performance de C/C++ sans ses inconvénients.

La Question : Et si le compilateur pouvait prévenir les bugs mémoire au moment de la compilation ?

➡️ Suivant : "Voir la partie 3 de cette article"

Retour au blog
Partager ::