Fehlende Speicherfreigabe nach effektiver Lebensdauer (Speicherleck)

Beschreibung

Fehlende Speicherfreigabe nach effektiver Lebensdauer tritt auf, wenn ein Programm Speicher alloziert, aber versäumt, ihn freizugeben, nachdem er nicht mehr benötigt wird. Dies führt zu Speicherlecks, bei denen der Speicherverbrauch des Programms kontinuierlich wächst. In lang laufenden Anwendungen können Speicherlecks schließlich den verfügbaren Speicher erschöpfen und Abstürze, Systeminstabilität oder Denial-of-Service verursachen. Speicherlecks sind besonders gefährlich in Servern, Daemons und eingebetteten Systemen, wo Anwendungen kontinuierlich laufen.

Risiko

Speicherlecks sind ein erhebliches Zuverlässigkeits- und Verfügbarkeitsproblem. In Webservern kann jede Anfrage, die Speicher leckt, schließlich den Server zum Absturz bringen. In eingebetteten Systemen mit begrenztem Speicher werden Lecks schnell kritisch. Speichererschöpfung kann zu Denial-of-Service führen. Lecks können auch andere Schwachstellen maskieren – Speicherkorruption stürzt möglicherweise nicht sofort ab, sondern erst wenn der Speicher fragmentiert wird. Moderne Systeme mit großen Mengen RAM können Lecks verbergen, bis sie plötzlich kritisch werden.

Lösung

Verwenden Sie Speicherverwaltungstools (Valgrind, AddressSanitizer) während der Entwicklung und beim Testen. Implementieren Sie konsistente Allokations-/Deallokationsmuster. Verwenden Sie RAII (Resource Acquisition Is Initialization) in C++ mit Smart Pointern. Erwägen Sie Garbage-Collection-Sprachen für komplexe Anwendungen. Implementieren Sie Referenzzählung für gemeinsam genutzte Ressourcen. Verwenden Sie statische Analysetools zur Leckerkennung. Überwachen Sie Produktionssysteme auf Speicherwachstum. Implementieren Sie ordnungsgemäße Fehlerbehandlung, die Allokationen auf Fehlerpfaden bereinigt.

Häufige Auswirkungen

AuswirkungDetails
VerfügbarkeitUmfang: Denial of Service

Kontinuierliche Speicherlecks erschöpfen schließlich den verfügbaren Speicher und bringen die Anwendung zum Absturz.
LeistungUmfang: Degradation

Speicherdruck verursacht Swapping, Garbage-Collection-Druck und allgemeine Verlangsamung.
ZuverlässigkeitUmfang: Systeminstabilität

Speichererschöpfung kann andere Prozesse auf demselben System beeinträchtigen.

Beispielcode

Anfälliger Code

// ANFÄLLIG: Speicher alloziert aber nie freigegeben
void process_request(const char *data) {
    char *buffer = malloc(1024);
    strcpy(buffer, data);
    process(buffer);
    // Fehlt: free(buffer);
    // Speicher leckt bei jedem Aufruf!
}

// ANFÄLLIG: Frühe Rückgabe ohne Freigabe
int read_config(const char *filename) {
    FILE *fp = fopen(filename, "r");
    if (!fp) return -1;

    char *buffer = malloc(4096);
    if (!buffer) {
        fclose(fp);
        return -1;
    }

    if (fread(buffer, 1, 4096, fp) == 0) {
        fclose(fp);
        return -1;  // Leckt buffer!
    }

    // Konfiguration verarbeiten...

    free(buffer);
    fclose(fp);
    return 0;
}

// ANFÄLLIG: Verlorener Pointer
void create_list(void) {
    Node *head = malloc(sizeof(Node));
    head->next = malloc(sizeof(Node));
    head->next->next = malloc(sizeof(Node));

    // head neu zuweisen - Referenz zu alloziertem Speicher verlieren!
    head = malloc(sizeof(Node));  // Ursprüngliche Kette leckt!
}

// ANFÄLLIG: Exception-ähnlicher Pfad leckt
int complex_operation(void) {
    void *resource1 = malloc(100);
    void *resource2 = malloc(200);
    void *resource3 = malloc(300);

    if (step1(resource1) != 0) {
        free(resource1);
        return -1;  // Leckt resource2, resource3!
    }

    if (step2(resource2) != 0) {
        free(resource1);
        free(resource2);
        return -1;  // Leckt resource3!
    }

    // ... fortsetzen ...
    free(resource1);
    free(resource2);
    free(resource3);
    return 0;
}
// ANFÄLLIG: C++ Roh-Pointer im Exception-Pfad
class DataProcessor {
    int* data;

public:
    DataProcessor() {
        data = new int[1000];
    }

    void process() {
        // Wenn Exception geworfen wird, wird Destruktor für lokale Objekte nicht aufgerufen
        DataProcessor* temp = new DataProcessor();
        throw std::runtime_error("Fehler");  // temp leckt!
        delete temp;
    }

    ~DataProcessor() {
        delete[] data;
    }
};

// ANFÄLLIG: Container von Roh-Zeigern
void process_items() {
    std::vector<Item*> items;

    for (int i = 0; i < 100; i++) {
        items.push_back(new Item());
    }

    // Vector zerstört aber Items nicht gelöscht - alle lecken!
}
# ANFÄLLIG: Python zirkulare Referenz (GC behandelt normalerweise, aber nicht immer)
class Node:
    def __init__(self):
        self.ref = None
        self.data = bytearray(1024 * 1024)  # 1MB

def create_cycle():
    a = Node()
    b = Node()
    a.ref = b
    b.ref = a  # Zirkulare Referenz
    # Mit benutzerdefiniertem __del__ sammelt Garbage Collector möglicherweise nicht

# ANFÄLLIG: Datei-Handles nicht geschlossen (Ressourcenleck)
def read_files(filenames):
    contents = []
    for fname in filenames:
        f = open(fname, 'r')  # Nie geschlossen!
        contents.append(f.read())
    return contents

Korrigierter Code

// SICHER: Allozierten Speicher immer freigeben
void process_request_safe(const char *data) {
    char *buffer = malloc(1024);
    if (!buffer) return;

    strcpy(buffer, data);
    process(buffer);
    free(buffer);  // Immer freigeben
}

// SICHER: Einzelner Cleanup-Pfad
int read_config_safe(const char *filename) {
    FILE *fp = NULL;
    char *buffer = NULL;
    int result = -1;

    fp = fopen(filename, "r");
    if (!fp) goto cleanup;

    buffer = malloc(4096);
    if (!buffer) goto cleanup;

    if (fread(buffer, 1, 4096, fp) == 0) {
        goto cleanup;  // Einzelner Cleanup-Pfad
    }

    // Konfiguration verarbeiten...
    result = 0;

cleanup:
    free(buffer);  // free(NULL) ist sicher
    if (fp) fclose(fp);
    return result;
}

// SICHER: Alle Allokationen verfolgen
void create_list_safe(void) {
    Node *head = malloc(sizeof(Node));
    if (!head) return;

    head->next = malloc(sizeof(Node));
    if (!head->next) {
        free(head);
        return;
    }

    head->next->next = malloc(sizeof(Node));
    if (!head->next->next) {
        free(head->next);
        free(head);
        return;
    }

    // Liste verwenden...

    // Gesamte Liste freigeben
    free(head->next->next);
    free(head->next);
    free(head);
}

// SICHER: Konsistentes Cleanup-Muster
typedef struct {
    void *resource1;
    void *resource2;
    void *resource3;
} Resources;

void cleanup_resources(Resources *r) {
    free(r->resource1);
    free(r->resource2);
    free(r->resource3);
    memset(r, 0, sizeof(*r));
}

int complex_operation_safe(void) {
    Resources r = {0};

    r.resource1 = malloc(100);
    r.resource2 = malloc(200);
    r.resource3 = malloc(300);

    if (!r.resource1 || !r.resource2 || !r.resource3) {
        cleanup_resources(&r);
        return -1;
    }

    if (step1(r.resource1) != 0) {
        cleanup_resources(&r);
        return -1;
    }

    if (step2(r.resource2) != 0) {
        cleanup_resources(&r);
        return -1;
    }

    // Erfolgspfad bereinigt auch
    cleanup_resources(&r);
    return 0;
}
// SICHER: Smart Pointer verwenden
#include <memory>
#include <vector>

class DataProcessorSafe {
    std::unique_ptr<int[]> data;

public:
    DataProcessorSafe() : data(std::make_unique<int[]>(1000)) {}

    void process() {
        // Smart Pointer behandelt Cleanup auch bei Exceptions
        auto temp = std::make_unique<DataProcessorSafe>();
        throw std::runtime_error("Fehler");  // temp wird automatisch bereinigt!
    }

    // Destruktor nicht nötig - unique_ptr behandelt es
};

// SICHER: Container von Smart Pointern
void process_items_safe() {
    std::vector<std::unique_ptr<Item>> items;

    for (int i = 0; i < 100; i++) {
        items.push_back(std::make_unique<Item>());
    }

    // Vector zerstört - alle Items automatisch gelöscht!
}

// SICHER: RAII-Wrapper für C-Ressourcen
class FileHandle {
    FILE* fp;
public:
    explicit FileHandle(const char* name, const char* mode)
        : fp(fopen(name, mode)) {
        if (!fp) throw std::runtime_error("Kann Datei nicht öffnen");
    }

    ~FileHandle() {
        if (fp) fclose(fp);
    }

    // Kopie löschen
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // Move erlauben
    FileHandle(FileHandle&& other) noexcept : fp(other.fp) {
        other.fp = nullptr;
    }

    FILE* get() { return fp; }
};

// Verwendung - automatischer Cleanup
void process_file(const char* filename) {
    FileHandle fh(filename, "r");
    // fh.get() verwenden...
    // Automatisch geschlossen wenn fh den Gültigkeitsbereich verlässt
}
# SICHER: Kontextmanager verwenden
def read_files_safe(filenames):
    contents = []
    for fname in filenames:
        with open(fname, 'r') as f:  # Automatisch geschlossen
            contents.append(f.read())
    return contents

# SICHER: Weak References um Zyklen zu brechen
import weakref

class Node:
    def __init__(self):
        self._ref = None
        self.data = bytearray(1024 * 1024)

    @property
    def ref(self):
        return self._ref() if self._ref else None

    @ref.setter
    def ref(self, value):
        self._ref = weakref.ref(value) if value else None

def create_no_cycle():
    a = Node()
    b = Node()
    a.ref = b  # Normale Referenz
    b.ref = a  # Weak Reference - verhindert Collection nicht

# SICHER: Expliziter Cleanup
class ManagedResource:
    def __init__(self):
        self.resource = allocate_large_resource()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.cleanup()

    def cleanup(self):
        if self.resource:
            release_resource(self.resource)
            self.resource = None

# Verwendung
with ManagedResource() as r:
    use_resource(r)
# Automatisch bereinigt

Ausgenutzt in der Praxis

Microsoft Windows LSASS Speicherleck (2003)

Speicherlecks in LSASS (Local Security Authority Subsystem Service) könnten aus der Ferne ausgelöst werden und verursachten schließlich Systemabstürze und Denial-of-Service.

OpenSSL Speicherleck DoS (2016)

CVE-2016-6304 war ein Speicherleck in OpenSSLs OCSP-Antwortverarbeitung, das für Denial-of-Service ausgenutzt werden könnte, indem wiederholte Statusanfragen gesendet wurden.

Firefox Speicherlecks

Mehrere Speicherlecks in Firefox wurden über die Jahre gemeldet, die den Speicherverbrauch des Browsers während länger Browsing-Sitzungen erheblich wachsen ließen.


Tools zum Testen/Ausnutzen


CVE-Beispiele


Referenzen

  1. MITRE. "CWE-401: Missing Release of Memory after Effective Lifetime." https://cwe.mitre.org/data/definitions/401.html

  2. CERT. "MEM31-C: Free dynamically allocated memory when no longer needed." https://wiki.sei.cmu.edu/confluence/display/c/MEM31-C.+Free+dynamically+allocated+memory+when+no+longer+needed