Zurück zur Startseite

Speicherlimit in Lua-Sandbox auf C++

Der Artikel beschreibt die Implementierung der Speicherkontrolle in Lua-Sandbox auf C++ mit einem benutzerdefinierten Allocator. Er behandelt architektonische Merkmale, Code-Beispiele und praktische Empfehlungen für Entwickler.

Speicherkontrolle in Lua: Skriptisolation auf C++
Advertisement 728x90

Begrenzung von Speicher und Ressourcen in einer Lua-Sandbox mit C++

Beim Einbinden von Lua in C++-Anwendungen besteht oft die Notwendigkeit, die Ausführung von Skripten vom Hauptsystem zu isolieren. Dies ist besonders entscheidend in Szenarien, in denen Code Dritter bösartig oder instabil sein könnte – etwa in Spieleenginen, Plugin-Systemen oder Sandbox-Umgebungen. Ein zentraler Aspekt solcher Isolation ist die Kontrolle des Speicherverbrauchs. Dieser Artikel erklärt, wie man eine strenge Grenze für den Speicherverbrauch in Lua umsetzt, indem man einen benutzerdefinierten Allokator in den Zustand des Interpreters integriert.

Wie Lua Speicher verwaltet

Lua überträgt alle Speicheroperationen an einen externen Allokator. Standardmäßig verwendet es realloc/free, aber die Sprache erlaubt es, diesen durch eine eigene Implementierung zu ersetzen. Dies geschieht, indem man beim Erstellen des lua_State einen Zeiger auf die Funktion übergibt. Wichtig: Alle Anfragen zur Zuweisung, zum Ändern der Größe und zum Freigeben von Speicher laufen über diesen Allokator – einschließlich der Operationen des Garbage Collectors, des Ladens von Modulen und der Erstellung von Tabellen, Strings, Funktionen usw.

Die Allokatorfunktion hat folgende Signatur:

Google AdInline article slot
void *luaAlloc(void *ud, void *ptr, size_t currSize, size_t newSize);
  • ud — Benutzerdaten (meist ein Zeiger auf den Zustand des Speicherkontrollers);
  • ptr — Zeiger auf den aktuellen Block (kann NULL sein bei Neuzuweisung);
  • currSize — aktuelle Blockgröße oder Objekttyp (in Lua 5.2+ bei ptr == NULL);
  • newSize — gewünschte neue Größe (0 bedeutet Freigeben).

Dies ermöglicht volle Kontrolle über die Speicherzuweisung und erlaubt es, deren Nutzung zu überwachen und zu begrenzen.

Implementierung eines begrenzten Allokators

Um die Menge des zugewiesenen Speichers zu verfolgen, führen wir eine Zustandsstruktur ein:

struct LimitedAllocatorState {
    size_t used {};
    size_t limit {1 * 1024 * 1024}; // 1 MB by default
    bool limitReached {false};
    bool overflow {false};

    bool isLimitEnabled() { return limit > 0; }
    void disableLimit() { limit = 0; }
    void resetErrorFlags() { limitReached = overflow = false; }
};

Der Allokator prüft, ob die neue Anfrage (used - currSize + newSize) die festgelegte Grenze überschreitet. Wenn ja, gibt er nullptr zurück, was in Lua einen Fehler auslöst (z. B. not enough memory).

Google AdInline article slot

Hier die wichtigsten Implementierungspunkte:

  • Bei newSize == 0 — Speicher wird freigegeben, used verringert sich um currSize;
  • Bei ptr == NULL — neuer Block wird zugewiesen, currSize wird ignoriert;
  • Schutz vor Integer-Überlauf ist zwingend erforderlich;
  • Der Allokatorzustand muss länger als lua_State existieren.

Beispiel-Funktion:

void *limitedAlloc(void *ud, void *ptr, size_t currSize, size_t newSize) {
    auto *allocState = static_cast<LimitedAllocatorState*>(ud);
    if (!allocState) return nullptr;

    if (ptr == nullptr) currSize = 0;
    if (newSize == 0) {
        if (ptr != nullptr) {
            allocState->used = (allocState->used >= currSize)
                ? allocState->used - currSize : 0;
        }
        std::free(ptr);
        return nullptr;
    }

    const size_t usedBase = (allocState->used >= currSize)
        ? allocState->used - currSize : 0;

    if (newSize > SIZE_MAX - usedBase) {
        allocState->overflow = true;
        return nullptr;
    }

    const size_t newUsed = usedBase + newSize;
    if (allocState->isLimitEnabled() && newUsed > allocState->limit) {
        allocState->limitReached = true;
        return nullptr;
    }

    void *newPtr = std::realloc(ptr, newSize);
    if (newPtr) allocState->used = newUsed;
    return newPtr;
}

Integration in die C++-Laufzeitumgebung

Beim Einsatz der sol2-Bibliothek (einem beliebten Wrapper über die Lua-C-API) sieht die Erstellung eines Zustands mit benutzerdefiniertem Allokator so aus:

Google AdInline article slot
sol::state lua(sol::default_at_panic, limitedAlloc, &allocState);

Wichtig ist die Reihenfolge der Felddeklarationen in der Klasse zu beachten: Der Allokatorzustand muss nach sol::state zerstört werden; andernfalls greift die Zerstörung des Zustands auf bereits freigegebenen Speicher zu.

Es empfiehlt sich, die gesamte Logik in eine LuaRuntime-Klasse zu packen, die:

  • Den Lebenszyklus des Zustands verwaltet;
  • Methoden zum Zurücksetzen von Fehlern und zum Ändern der Grenze zur Laufzeit bietet;
  • Erlaubt, zu prüfen, ob das Kontingent überschritten wurde.

Beispiel-Interface:

class LuaRuntime {
private:
    lua::memory::LimitedAllocatorState allocatorState;
    sol::state state;

public:
    LuaRuntime(size_t memoryLimit)
        : allocatorState({.limit = memoryLimit}),
          state(sol::default_at_panic, lua::memory::limitedAlloc, &allocatorState) {}

    bool hasAllocError() {
        return allocatorState.limitReached || allocatorState.overflow;
    }

    void resetAllocErrors() {
        allocatorState.resetErrorFlags();
    }

    bool setMemoryLimit(size_t limit) {
        allocatorState.limit = limit;
        return true;
    }
};

Einschränkungen und praktische Empfehlungen

Einige wichtige Hinweise beim Arbeiten mit einem begrenzten Allokator:

  • Die Grenze gilt für den gesamten Zustand. Wenn Sie mehrere Sandboxes auf einem lua_State nutzen, teilen sie sich ein einziges Speicherkontingent. Für strenge Isolation – einen Zustand pro Sandbox verwenden.
  • Der Garbage Collector verbraucht ebenfalls Speicher. Selbst nach dem Löschen von Objekten in Lua kann der Speicherverbrauch während GC-Läufen vorübergehend steigen.
  • Zuweisungsfehler lösen nicht immer eine Panik aus. Lua erzeugt eine Ausnahme, die über pcall oder einen Panic-Handler abgefangen werden kann.
  • Vergessen Sie nicht, Fehlermarkierungen zurückzusetzen. Nach der Fehlerbehandlung bleibt limitReached gesetzt, bis es explizit zurückgesetzt wird.

Beachten Sie auch, dass bestimmte Operationen (z. B. Verkettung langer Strings oder tiefe Rekursion) zu plötzlichen Spitzen im Speicherverbrauch führen können. Tests mit realistischen Workloads sind unerlässlich.

Wichtige Erkenntnisse

  • Lua erlaubt volle Kontrolle über die Speicherzuweisung durch einen benutzerdefinierten Allokator.
  • Die Speichergrenze gilt für den gesamten Zustand, nicht für einzelne Skripte.
  • Bei Überschreitung der Grenze gibt der Allokator nullptr zurück und löst in Lua einen Fehler aus.
  • Der Allokatorzustand muss länger als lua_State existieren.
  • Schutz vor Integer-Überlauf und korrekte Arithmetik sind für eine zuverlässige Implementierung essenziell.

— Editorial Team

Advertisement 728x90

Weiterlesen