Verwendung des Singleton-Musters ohne Synchronisation in Multithread-Kontext

Beschreibung

Verwendung des Singleton-Musters ohne Synchronisation in Multithread-Kontext ist eine Schwachstelle, bei der ein Produkt das Singleton-Muster bei der Ressourcenerstellung in Multithread-Umgebungen ohne ordnungsgemäße Thread-Sicherheitsmechanismen implementiert. Die klassische Singleton-Implementierung mit einer Null-Prüfung und Instanzerstellung ist nicht atomar, was mehreren Threads ermöglicht, die Null-Prüfung gleichzeitig zu passieren und jeweils separate Instanzen zu erstellen. Dies verletzt den Singleton-Vertrag, kann Dateninkonsistenz verursachen, wenn jede Instanz unterschiedlichen Zustand pflegt, und kann zu Ressourcenlecks durch doppelte Initialisierung führen.

Risiko

Thread-unsichere Singletons erzeugen subtile Nebenläufigkeitsfehler, die schwer zu reproduzieren und zu debuggen sind. Wenn mehrere Instanzen erstellt werden, sind Zustandsänderungen in einer Instanz möglicherweise nicht für Code sichtbar, der eine andere Instanz verwendet, was inkonsistentes Verhalten verursacht. In Sicherheitskontexten kann dies zu umgangenen Authentifizierungsprüfungen, duplizierten Zugriffstokens oder Race-Conditions in Autorisierungslogik führen. Ressourcenverwaltungs-Singletons können Verbindungslecks oder Dateihandle-Erschöpfung verursachen. Konfigurations-Singletons können veraltete oder inkonsistente Einstellungen zurückgeben. Das Double-Checked-Locking-Muster, das oft als "Fix" verwendet wird, ist selbst in Java vor Version 5 und C++ vor C++11 aufgrund von Speichermodell-Problemen fehlerhaft.

Lösung

Verwenden Sie sprachspezifische thread-sichere Singleton-Implementierungen. In Java verwenden Sie Eager-Initialisierung mit static final, Enum-basierte Singletons oder synchronisierte getInstance()-Methoden. Für Lazy-Initialisierung in Java 5+ verwenden Sie das volatile-Schlüsselwort mit Double-Checked-Locking. Erwägen Sie die Verwendung des Initialization-on-Demand-Holder-Idioms. In C++11 und später verwenden Sie statische lokale Variablen, die garantiert thread-sicher sind. Alternativ verwenden Sie Dependency-Injection-Frameworks, die den Singleton-Scope verwalten. Vermeiden Sie das Speichern von veränderbaren benutzerspezifischen Daten in Singletons. Wenn thread-lokale Speicherung benötigt wird, verwenden Sie ThreadLocal statt Singletons.

Häufige Auswirkungen

AuswirkungDetails
IntegritätBereich: Integrität

Anwendungsdaten ändern - Mehrere Singleton-Instanzen können erstellt werden, jede mit unterschiedlichem Zustand, was zu Dateninkonsistenz führt, wenn Code auf verschiedenen Instanzen operiert.
SonstigeBereich: Sonstige

Qualitätsverschlechterung - Der Singleton-Vertrag wird verletzt, was zu unvorhersehbarem Verhalten führt, das von Thread-Timing abhängt und schwer zu reproduzieren oder zu debuggen ist.

Beispielcode + Lösungscode

Verwundbarer Code

// Verwundbar: Klassischer Lazy-Singleton ohne Synchronisation
public class VerwundbaresSingleton {
    private static VerwundbaresSingleton instance;
    private int counter = 0;

    private VerwundbaresSingleton() {
        // Singleton initialisieren
        System.out.println("Singleton erstellt");
    }

    // Verwundbar: Nicht thread-sicher
    public static VerwundbaresSingleton getInstance() {
        if (instance == null) {  // Thread A prüft, sieht null
            // Thread B prüft auch, sieht ebenfalls null
            instance = new VerwundbaresSingleton();  // Beide erstellen Instanzen!
        }
        return instance;  // Verschiedene Threads können verschiedene Instanzen erhalten
    }

    public void increment() {
        counter++;
    }

    public int getCounter() {
        return counter;
    }
}

// Demonstration der Race-Condition:
// Thread A: prüft instance == null, wahr
// Thread A: beginnt neue Instanz zu erstellen
// Thread B: prüft instance == null, immer noch wahr (noch nicht zugewiesen)
// Thread B: erstellt eine weitere neue Instanz
// Thread A: weist erste Instanz der Variable zu
// Thread B: überschreibt mit zweiter Instanz
// Ergebnis: zwei Instanzen erstellt, erste Instanz verwaist

// Verwundbar: Double-Checked-Locking ohne volatile (pre-Java-5-Stil)
public class VerwundbaresDoubleChecked {
    private static VerwundbaresDoubleChecked instance;  // Fehlendes volatile!

    public static VerwundbaresDoubleChecked getInstance() {
        if (instance == null) {
            synchronized (VerwundbaresDoubleChecked.class) {
                if (instance == null) {
                    instance = new VerwundbaresDoubleChecked();
                    // Verwundbar: Zuweisung kann umgeordnet werden
                    // Ein anderer Thread könnte nicht-null-Referenz
                    // zu teilweise konstruiertem Objekt sehen!
                }
            }
        }
        return instance;
    }
}
// Verwundbar: C++-Singleton ohne Synchronisation
class VerwundbaresSingleton {
private:
    static VerwundbaresSingleton* instance;
    int data;

    VerwundbaresSingleton() : data(0) {}

public:
    // Verwundbar: Race-Condition in getInstance
    static VerwundbaresSingleton* getInstance() {
        if (instance == nullptr) {  // Nicht thread-sicher!
            instance = new VerwundbaresSingleton();
        }
        return instance;
    }

    void setData(int d) { data = d; }
    int getData() { return data; }
};

VerwundbaresSingleton* VerwundbaresSingleton::instance = nullptr;

// Verwundbar: Double-Checked-Locking (fehlerhaft pre-C++11)
class VerwundbaresDoubleChecked {
private:
    static VerwundbaresDoubleChecked* instance;
    static pthread_mutex_t mutex;

public:
    static VerwundbaresDoubleChecked* getInstance() {
        if (instance == nullptr) {
            pthread_mutex_lock(&mutex);
            if (instance == nullptr) {
                instance = new VerwundbaresDoubleChecked();
                // Verwundbar: Speicherumordnung kann bewirken,
                // dass ein anderer Thread nicht-null-Pointer
                // zu teilweise konstruiertem Objekt sieht
            }
            pthread_mutex_unlock(&mutex);
        }
        return instance;
    }
};
# Verwundbar: Python-Singleton ohne Locking
class VerwundbaresSingleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:  # Race-Condition!
            # Thread-Wechsel könnte hier passieren
            cls._instance = super().__new__(cls)
            # Ein anderer Thread könnte auch eine Instanz erstellen
        return cls._instance

# Verwundbar: Modul-Level-Singleton ohne Thread-Sicherheit
class VerwundbareConfig:
    _instance = None
    _config = {}

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            # Verwundbar: Mehrere Threads können hier ankommen
            cls._instance = cls()
            cls._instance._load_config()
        return cls._instance

    def _load_config(self):
        # Teure Initialisierung, die nicht zweimal passieren sollte
        self._config = load_from_file()
// Verwundbar: Node.js-Singleton in asynchroner Umgebung
class VerwundbaresSingleton {
    static instance = null;

    static async getInstance() {
        if (VerwundbaresSingleton.instance === null) {
            // Verwundbar: Mehrere gleichzeitige Aufrufe können diese Prüfung passieren
            VerwundbaresSingleton.instance = new VerwundbaresSingleton();
            await VerwundbaresSingleton.instance.initialize();
            // initialize() ist async, Race-Condition während await
        }
        return VerwundbaresSingleton.instance;
    }

    async initialize() {
        // Teure asynchrone Initialisierung
        this.data = await loadConfiguration();
    }
}

Lösungscode

// Behoben: Thread-sichere Singleton-Implementierungen in Java

// Option 1: Eager-Initialisierung (einfachste, immer thread-sicher)
public class EagerSingleton {
    // Behoben: Static final garantiert thread-sichere Initialisierung
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {}

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

// Option 2: Enum-Singleton (empfohlen von Effective Java)
public enum EnumSingleton {
    INSTANCE;

    private int counter = 0;

    public void increment() { counter++; }
    public int getCounter() { return counter; }
}
// Verwendung: EnumSingleton.INSTANCE.increment();

// Option 3: Double-Checked-Locking mit volatile (Java 5+)
public class VolatileSingleton {
    // Behoben: volatile stellt Sichtbarkeit sicher und verhindert Umordnung
    private static volatile VolatileSingleton instance;

    private VolatileSingleton() {}

    public static VolatileSingleton getInstance() {
        if (instance == null) {
            synchronized (VolatileSingleton.class) {
                if (instance == null) {
                    instance = new VolatileSingleton();
                }
            }
        }
        return instance;
    }
}

// Option 4: Initialization-on-Demand-Holder-Idiom
public class HolderSingleton {
    private HolderSingleton() {}

    // Behoben: Statische Holder-Klasse bietet lazy, thread-sichere Initialisierung
    private static class Holder {
        private static final HolderSingleton INSTANCE = new HolderSingleton();
    }

    public static HolderSingleton getInstance() {
        return Holder.INSTANCE;  // Klassenladen ist thread-sicher
    }
}

// Option 5: Synchronisierte Methode (einfach aber potenziell langsamer)
public class SynchronizedSingleton {
    private static SynchronizedSingleton instance;

    private SynchronizedSingleton() {}

    // Behoben: synchronized stellt sicher, dass nur ein Thread gleichzeitig
    public static synchronized SynchronizedSingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedSingleton();
        }
        return instance;
    }
}
// Behoben: C++11 thread-sicherer Singleton

// Option 1: Meyers' Singleton (C++11 garantiert Thread-Sicherheit)
class MeyersSingleton {
public:
    // Behoben: C++11 garantiert, dass statisch lokal thread-sicher initialisiert wird
    static MeyersSingleton& getInstance() {
        static MeyersSingleton instance;
        return instance;
    }

    // Copy- und Move-Konstruktoren löschen
    MeyersSingleton(const MeyersSingleton&) = delete;
    MeyersSingleton& operator=(const MeyersSingleton&) = delete;
    MeyersSingleton(MeyersSingleton&&) = delete;
    MeyersSingleton& operator=(MeyersSingleton&&) = delete;

private:
    MeyersSingleton() {}
};

// Option 2: Double-Checked-Locking mit atomic (C++11)
#include <atomic>
#include <mutex>

class AtomicSingleton {
private:
    static std::atomic<AtomicSingleton*> instance;
    static std::mutex mtx;

public:
    static AtomicSingleton* getInstance() {
        AtomicSingleton* tmp = instance.load(std::memory_order_acquire);
        if (tmp == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            tmp = instance.load(std::memory_order_relaxed);
            if (tmp == nullptr) {
                tmp = new AtomicSingleton();
                instance.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }
};

std::atomic<AtomicSingleton*> AtomicSingleton::instance{nullptr};
std::mutex AtomicSingleton::mtx;

// Option 3: call_once (C++11)
#include <memory>

class CallOnceSingleton {
private:
    static std::unique_ptr<CallOnceSingleton> instance;
    static std::once_flag initFlag;

public:
    static CallOnceSingleton& getInstance() {
        std::call_once(initFlag, []() {
            instance.reset(new CallOnceSingleton());
        });
        return *instance;
    }
};

std::unique_ptr<CallOnceSingleton> CallOnceSingleton::instance;
std::once_flag CallOnceSingleton::initFlag;
# Behoben: Python thread-sichere Singleton-Implementierungen
import threading

# Option 1: Thread-sicherer Singleton mit Lock
class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:  # Behoben: Lock erwerben
                # Double-Check innerhalb des Locks
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance


# Option 2: Verwendung von Modul-Level-Variable (Pythonischer Ansatz)
# singleton.py
class _Singleton:
    def __init__(self):
        self.data = {}

# Behoben: Modul-Level-Instanz bei Import erstellt (thread-sicher)
singleton_instance = _Singleton()

# Andere Module: from singleton import singleton_instance


# Option 3: Metaclass-basierter Singleton
class SingletonMeta(type):
    _instances = {}
    _lock = threading.Lock()

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            with cls._lock:
                if cls not in cls._instances:
                    instance = super().__call__(*args, **kwargs)
                    cls._instances[cls] = instance
        return cls._instances[cls]


class MySingleton(metaclass=SingletonMeta):
    def __init__(self):
        self.value = 0


# Option 4: Verwendung von functools.lru_cache
from functools import lru_cache

class ConfigSingleton:
    def __init__(self):
        self.config = self._load_config()

    def _load_config(self):
        return {'key': 'value'}


@lru_cache(maxsize=1)
def get_config_singleton():
    """Behoben: lru_cache stellt einmalige Erstellung sicher."""
    return ConfigSingleton()
// Behoben: Node.js thread-sichere Singleton-Muster

// Option 1: Modul-Caching (Node.js cached Module)
// singleton.js
class Singleton {
    constructor() {
        this.data = {};
    }

    static instance = null;

    static getInstance() {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
}

// Behoben: Modul-Exports stellen einzelne Instanz sicher
module.exports = Singleton.getInstance();

// Option 2: Async-Singleton mit Promise
class AsyncSingleton {
    static instance = null;
    static initializationPromise = null;

    static async getInstance() {
        if (AsyncSingleton.instance) {
            return AsyncSingleton.instance;
        }

        // Behoben: Promise verwenden, um doppelte Initialisierung zu verhindern
        if (!AsyncSingleton.initializationPromise) {
            AsyncSingleton.initializationPromise = (async () => {
                const instance = new AsyncSingleton();
                await instance.initialize();
                AsyncSingleton.instance = instance;
                return instance;
            })();
        }

        return AsyncSingleton.initializationPromise;
    }

    async initialize() {
        this.data = await loadConfiguration();
    }
}

// Option 3: Eingefrorener Singleton
const frozenSingleton = Object.freeze({
    data: {},
    getValue(key) { return this.data[key]; },
    setValue(key, value) { this.data[key] = value; }
});

module.exports = frozenSingleton;

CVE-Beispiele

Keine spezifischen CVEs sind in der MITRE-Datenbank für dieses CWE gelistet. Das Schwachstellenmuster ist jedoch dokumentiert in:

  • Nebenläufigkeitsfehler-Forschung
  • Java-Speichermodell-Spezifikationen
  • C++-Standardkomitee-Papers

Referenzen

  1. MITRE Corporation. "CWE-543: Use of Singleton Pattern Without Synchronization in a Multithreaded Context." https://cwe.mitre.org/data/definitions/543.html
  2. Bloch, Joshua. "Effective Java" - Item 3: Enforce the singleton property.
  3. CERT Oracle Secure Coding Standard for Java. "LCK10-J. Use a correct form of the double-checked locking idiom."