memory-safety-patterns

Par wshobson · agents

Implémentez une programmation sûre en mémoire avec RAII, ownership, smart pointers et gestion des ressources en Rust, C++ et C. À utiliser lors de l'écriture de code système sûr, de la gestion des ressources ou de la prévention des bugs mémoire.

npx skills add https://github.com/wshobson/agents --skill memory-safety-patterns

Motifs de sécurité mémoire

Motifs cross-language pour la programmation mémoire-sûre incluant RAII, ownership, smart pointers, et gestion des ressources.

Quand utiliser cette compétence

  • Écrire du code système mémoire-sûr
  • Gérer les ressources (fichiers, sockets, mémoire)
  • Prévenir use-after-free et fuites mémoire
  • Implémenter des motifs RAII
  • Choisir entre langages pour la sécurité
  • Déboguer les problèmes mémoire

Concepts fondamentaux

1. Catégories de bugs mémoire

Type de bug Description Prévention
Use-after-free Accès à la mémoire libérée Ownership, RAII
Double-free Libération deux fois de la mémoire Smart pointers
Fuite mémoire Jamais libérer la mémoire RAII, GC
Débordement buffer Écriture au-delà de la fin du buffer Vérification limites
Pointeur dangling Pointeur vers mémoire libérée Suivi de durée de vie
Course aux données Accès concurrent non synchronisé Ownership, Sync

2. Spectre de sécurité

Manual (C) → Smart Pointers (C++) → Ownership (Rust) → GC (Go, Java)
Moins sûr                                              Plus sûr
Plus de contrôle                                       Moins de contrôle

Motifs par langage

Motif 1 : RAII en C++

// RAII: Resource Acquisition Is Initialization
// Durée de vie des ressources liée à la durée de vie de l'objet

#include <memory>
#include <fstream>
#include <mutex>

// Descripteur de fichier avec RAII
class FileHandle {
public:
    explicit FileHandle(const std::string& path)
        : file_(path) {
        if (!file_.is_open()) {
            throw std::runtime_error("Failed to open file");
        }
    }

    // Destructeur ferme automatiquement le fichier
    ~FileHandle() = default; // fstream ferme dans son destructeur

    // Supprimer la copie (prévenir double-fermeture)
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // Autoriser le move
    FileHandle(FileHandle&&) = default;
    FileHandle& operator=(FileHandle&&) = default;

    void write(const std::string& data) {
        file_ << data;
    }

private:
    std::fstream file_;
};

// Lock guard (RAII pour les mutexes)
class Database {
public:
    void update(const std::string& key, const std::string& value) {
        std::lock_guard<std::mutex> lock(mutex_); // Libéré à la sortie du scope
        data_[key] = value;
    }

    std::string get(const std::string& key) {
        std::shared_lock<std::shared_mutex> lock(shared_mutex_);
        return data_[key];
    }

private:
    std::mutex mutex_;
    std::shared_mutex shared_mutex_;
    std::map<std::string, std::string> data_;
};

// Transaction avec rollback (RAII)
template<typename T>
class Transaction {
public:
    explicit Transaction(T& target)
        : target_(target), backup_(target), committed_(false) {}

    ~Transaction() {
        if (!committed_) {
            target_ = backup_; // Rollback
        }
    }

    void commit() { committed_ = true; }

    T& get() { return target_; }

private:
    T& target_;
    T backup_;
    bool committed_;
};

Motif 2 : Smart pointers en C++

#include <memory>

// unique_ptr: Ownership unique
class Engine {
public:
    void start() { /* ... */ }
};

class Car {
public:
    Car() : engine_(std::make_unique<Engine>()) {}

    void start() {
        engine_->start();
    }

    // Transférer l'ownership
    std::unique_ptr<Engine> extractEngine() {
        return std::move(engine_);
    }

private:
    std::unique_ptr<Engine> engine_;
};

// shared_ptr: Ownership partagé
class Node {
public:
    std::string data;
    std::shared_ptr<Node> next;

    // Utiliser weak_ptr pour casser les cycles
    std::weak_ptr<Node> parent;
};

void sharedPtrExample() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();

    node1->next = node2;
    node2->parent = node1; // Référence faible prévient le cycle

    // Accès weak_ptr
    if (auto parent = node2->parent.lock()) {
        // parent est un shared_ptr valide
    }
}

// Deleter personnalisé pour les ressources
class Socket {
public:
    static void close(int* fd) {
        if (fd && *fd >= 0) {
            ::close(*fd);
            delete fd;
        }
    }
};

auto createSocket() {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    return std::unique_ptr<int, decltype(&Socket::close)>(
        new int(fd),
        &Socket::close
    );
}

// Bonnes pratiques make_unique/make_shared
void bestPractices() {
    // Bon : Exception-safe, allocation simple
    auto ptr = std::make_shared<Widget>();

    // Mauvais : Deux allocations, pas exception-safe
    std::shared_ptr<Widget> ptr2(new Widget());

    // Pour les tableaux
    auto arr = std::make_unique<int[]>(10);
}

Motif 3 : Ownership en Rust

// Sémantique move (défaut)
fn move_example() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 est DÉPLACÉ, plus valide

    // println!("{}", s1); // Erreur de compilation !
    println!("{}", s2);
}

// Borrowing (références)
fn borrow_example() {
    let s = String::from("hello");

    // Emprunt immutable (plusieurs autorisés)
    let len = calculate_length(&s);
    println!("{} has length {}", s, len);

    // Emprunt mutable (un seul autorisé)
    let mut s = String::from("hello");
    change(&mut s);
}

fn calculate_length(s: &String) -> usize {
    s.len()
} // s sort du scope, mais n'est pas droppé puisqu'emprunté

fn change(s: &mut String) {
    s.push_str(", world");
}

// Lifetimes: Le compilateur suit la validité des références
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// Struct avec références a besoin d'annotation de lifetime
struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }

    // Lifetime elision: compilateur déduit 'a pour &self
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention: {}", announcement);
        self.part
    }
}

// Interior mutability
use std::cell::{Cell, RefCell};
use std::rc::Rc;

struct Stats {
    count: Cell<i32>,           // Types Copy
    data: RefCell<Vec<String>>, // Types non-Copy
}

impl Stats {
    fn increment(&self) {
        self.count.set(self.count.get() + 1);
    }

    fn add_data(&self, item: String) {
        self.data.borrow_mut().push(item);
    }
}

// Rc pour ownership partagé (single-threaded)
fn rc_example() {
    let data = Rc::new(vec![1, 2, 3]);
    let data2 = Rc::clone(&data); // Incrémenter le compte de références

    println!("Count: {}", Rc::strong_count(&data)); // 2
}

// Arc pour ownership partagé (thread-safe)
use std::sync::Arc;
use std::thread;

fn arc_example() {
    let data = Arc::new(vec![1, 2, 3]);

    let handles: Vec<_> = (0..3)
        .map(|_| {
            let data = Arc::clone(&data);
            thread::spawn(move || {
                println!("{:?}", data);
            })
        })
        .collect();

    for handle in handles {
        handle.join().unwrap();
    }
}

Motif 4 : Gestion sûre des ressources en C

// C n'a pas RAII, mais on peut utiliser des motifs

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

// Motif : goto cleanup
int process_file(const char* path) {
    FILE* file = NULL;
    char* buffer = NULL;
    int result = -1;

    file = fopen(path, "r");
    if (!file) {
        goto cleanup;
    }

    buffer = malloc(1024);
    if (!buffer) {
        goto cleanup;
    }

    // Traiter le fichier...
    result = 0;

cleanup:
    if (buffer) free(buffer);
    if (file) fclose(file);
    return result;
}

// Motif : Pointeur opaque avec create/destroy
typedef struct Context Context;

Context* context_create(void);
void context_destroy(Context* ctx);
int context_process(Context* ctx, const char* data);

// Implémentation
struct Context {
    int* data;
    size_t size;
    FILE* log;
};

Context* context_create(void) {
    Context* ctx = calloc(1, sizeof(Context));
    if (!ctx) return NULL;

    ctx->data = malloc(100 * sizeof(int));
    if (!ctx->data) {
        free(ctx);
        return NULL;
    }

    ctx->log = fopen("log.txt", "w");
    if (!ctx->log) {
        free(ctx->data);
        free(ctx);
        return NULL;
    }

    return ctx;
}

void context_destroy(Context* ctx) {
    if (ctx) {
        if (ctx->log) fclose(ctx->log);
        if (ctx->data) free(ctx->data);
        free(ctx);
    }
}

// Motif : Cleanup attribute (extension GCC/Clang)
#define AUTO_FREE __attribute__((cleanup(auto_free_func)))

void auto_free_func(void** ptr) {
    free(*ptr);
}

void auto_free_example(void) {
    AUTO_FREE char* buffer = malloc(1024);
    // buffer automatiquement libéré à la fin du scope
}

Motif 5 : Vérification des limites

// C++ : Utiliser les conteneurs au lieu des tableaux bruts
#include <vector>
#include <array>
#include <span>

void safe_array_access() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    // Sûr : lance std::out_of_range
    try {
        int val = vec.at(10);
    } catch (const std::out_of_range& e) {
        // Gérer l'erreur
    }

    // Non sûr mais plus rapide (pas de vérification)
    int val = vec[2];

    // C++20 moderne : std::span pour les vues de tableaux
    std::span<int> view(vec);
    // Les itérateurs sont bounds-safe
    for (int& x : view) {
        x *= 2;
    }
}

// Tableaux de taille fixe
void fixed_array() {
    std::array<int, 5> arr = {1, 2, 3, 4, 5};

    // Taille connue à la compilation
    static_assert(arr.size() == 5);

    // Accès sûr
    int val = arr.at(2);
}
// Rust : Vérification des limites par défaut

fn rust_bounds_checking() {
    let vec = vec![1, 2, 3, 4, 5];

    // Vérification des limites à l'exécution (panic si hors limites)
    let val = vec[2];

    // Option explicite (pas de panic)
    match vec.get(10) {
        Some(val) => println!("Got {}", val),
        None => println!("Index out of bounds"),
    }

    // Itérateurs (pas de vérification nécessaire)
    for val in &vec {
        println!("{}", val);
    }

    // Les slices sont vérifiées aux limites
    let slice = &vec[1..3]; // [2, 3]
}

Motif 6 : Prévention des courses aux données

// C++ : État partagé thread-safe
#include <mutex>
#include <shared_mutex>
#include <atomic>

class ThreadSafeCounter {
public:
    void increment() {
        // Opérations atomiques
        count_.fetch_add(1, std::memory_order_relaxed);
    }

    int get() const {
        return count_.load(std::memory_order_relaxed);
    }

private:
    std::atomic<int> count_{0};
};

class ThreadSafeMap {
public:
    void write(const std::string& key, int value) {
        std::unique_lock lock(mutex_);
        data_[key] = value;
    }

    std::optional<int> read(const std::string& key) {
        std::shared_lock lock(mutex_);
        auto it = data_.find(key);
        if (it != data_.end()) {
            return it->second;
        }
        return std::nullopt;
    }

private:
    mutable std::shared_mutex mutex_;
    std::map<std::string, int> data_;
};
// Rust : Prévention de course aux données à la compilation

use std::sync::{Arc, Mutex, RwLock};
use std::sync::atomic::{AtomicI32, Ordering};
use std::thread;

// Atomic pour les types simples
fn atomic_example() {
    let counter = Arc::new(AtomicI32::new(0));

    let handles: Vec<_> = (0..10)
        .map(|_| {
            let counter = Arc::clone(&counter);
            thread::spawn(move || {
                counter.fetch_add(1, Ordering::SeqCst);
            })
        })
        .collect();

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Counter: {}", counter.load(Ordering::SeqCst));
}

// Mutex pour les types complexes
fn mutex_example() {
    let data = Arc::new(Mutex::new(vec![]));

    let handles: Vec<_> = (0..10)
        .map(|i| {
            let data = Arc::clone(&data);
            thread::spawn(move || {
                let mut vec = data.lock().unwrap();
                vec.push(i);
            })
        })
        .collect();

    for handle in handles {
        handle.join().unwrap();
    }
}

// RwLock pour les charges de travail heavy en lecture
fn rwlock_example() {
    let data = Arc::new(RwLock::new(HashMap::new()));

    // Plusieurs lecteurs OK
    let read_guard = data.read().unwrap();

    // Le writer bloque les lecteurs
    let write_guard = data.write().unwrap();
}

Bonnes pratiques

À faire

  • Préférer RAII - Lier la durée de vie des ressources au scope
  • Utiliser les smart pointers - Éviter les pointeurs bruts en C++
  • Comprendre l'ownership - Savoir qui possède quoi
  • Vérifier les limites - Utiliser les méthodes d'accès sûr
  • Utiliser les outils - AddressSanitizer, Valgrind, Miri

À ne pas faire

  • Ne pas utiliser les pointeurs bruts - Sauf interface avec C
  • Ne pas retourner les références locales - Pointeur dangling
  • Ne pas ignorer les avertissements du compilateur - Ils détectent des bugs
  • Ne pas utiliser unsafe à la légère - En Rust, le minimiser
  • Ne pas supposer la thread-safety - Être explicite

Outils de débogage

# AddressSanitizer (Clang/GCC)
clang++ -fsanitize=address -g source.cpp

# Valgrind
valgrind --leak-check=full ./program

# Rust Miri (détecteur de comportement indéfini)
cargo +nightly miri run

# ThreadSanitizer
clang++ -fsanitize=thread -g source.cpp

Skills similaires