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:
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 (kannNULLsein bei Neuzuweisung);currSize— aktuelle Blockgröße oder Objekttyp (in Lua 5.2+ beiptr == 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).
Hier die wichtigsten Implementierungspunkte:
- Bei
newSize == 0— Speicher wird freigegeben,usedverringert sich umcurrSize; - Bei
ptr == NULL— neuer Block wird zugewiesen,currSizewird ignoriert; - Schutz vor Integer-Überlauf ist zwingend erforderlich;
- Der Allokatorzustand muss länger als
lua_Stateexistieren.
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:
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_Statenutzen, 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
pcalloder einen Panic-Handler abgefangen werden kann. - Vergessen Sie nicht, Fehlermarkierungen zurückzusetzen. Nach der Fehlerbehandlung bleibt
limitReachedgesetzt, 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
nullptrzurück und löst in Lua einen Fehler aus. - Der Allokatorzustand muss länger als
lua_Stateexistieren. - Schutz vor Integer-Überlauf und korrekte Arithmetik sind für eine zuverlässige Implementierung essenziell.
— Editorial Team
Noch keine Kommentare.