# Limitation de la mémoire et des ressources dans un bac à sable Lua avec C++
Lors de l'intégration de Lua dans des applications C++, il est souvent nécessaire d'isoler l'exécution des scripts du système principal. Cela est particulièrement critique dans les scénarios où le code tiers pourrait être malveillant ou instable — par exemple, dans les moteurs de jeu, les systèmes de plugins ou les environnements en bac à sable. Un aspect clé de cette isolation est le contrôle de la consommation de mémoire. Cet article explique comment implémenter une limite stricte d'utilisation de la mémoire dans Lua à l'aide d'un allocateur personnalisé intégré à l'état de l'interpréteur.
Comment Lua gère la mémoire
Lua délègue toutes les opérations de mémoire à un allocateur externe. Par défaut, il utilise les fonctions standard realloc/free, mais le langage permet de le remplacer par votre propre implémentation. Cela s'obtient en passant un pointeur vers la fonction lors de la création de lua_State. Important : toutes les demandes d'allocation, de redimensionnement et de libération passent par cet allocateur — y compris les opérations du ramasse-miettes, le chargement de modules, et la création de tables, chaînes, fonctions, etc.
La fonction d'allocation a la signature suivante :
void *luaAlloc(void *ud, void *ptr, size_t currSize, size_t newSize);
ud— données utilisateur (généralement un pointeur vers l'état du contrôleur de mémoire) ;ptr— pointeur vers le bloc actuel (peut êtreNULLlors d'une allocation nouvelle) ;currSize— taille actuelle du bloc ou type d'objet (dans Lua 5.2+ quandptr == NULL) ;newSize— nouvelle taille demandée (0 signifie libération).
Cela donne un contrôle total sur l'allocation mémoire, permettant de suivre et de limiter son utilisation.
Implémentation d'un allocateur limité
Pour suivre la quantité de mémoire allouée, nous introduisons une structure d'état :
struct LimitedAllocatorState {
size_t used {};
size_t limit {1 * 1024 * 1024}; // 1 Mo par défaut
bool limitReached {false};
bool overflow {false};
bool isLimitEnabled() { return limit > 0; }
void disableLimit() { limit = 0; }
void resetErrorFlags() { limitReached = overflow = false; }
};
L'allocateur vérifie si la nouvelle demande (used - currSize + newSize) dépasse la limite définie. Si c'est le cas, il retourne nullptr, ce qui déclenche une erreur dans Lua (par ex., not enough memory).
Voici les points clés de l'implémentation :
- Quand
newSize == 0— la mémoire est libérée, etuseddiminue decurrSize; - Quand
ptr == NULL— un nouveau bloc est alloué,currSizeest ignoré ; - La protection contre le débordement d'entiers est obligatoire ;
- L'état de l'allocateur doit survivre à
lua_State.
Exemple de fonction :
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;
}
Intégration avec l'environnement d'exécution C++
Lors de l'utilisation de la bibliothèque sol2 (un wrapper populaire sur l'API C de Lua), la création d'un état avec un allocateur personnalisé ressemble à ceci :
sol::state lua(sol::default_at_panic, limitedAlloc, &allocState);
Il est important de respecter l'ordre des déclarations de champs dans la classe : l'état de l'allocateur doit être détruit après sol::state ; sinon, la destruction de l'état accédera à une mémoire déjà libérée.
Il est recommandé d'encapsuler toute la logique dans une classe LuaRuntime qui :
- Gère le cycle de vie de l'état ;
- Fournit des méthodes pour réinitialiser les erreurs et modifier la limite à la volée ;
- Permet de vérifier si le quota a été dépassé.
Interface d'exemple :
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;
}
};
Limitations et recommandations pratiques
Quelques notes importantes lors de l'utilisation d'un allocateur limité :
- La limite est partagée sur l'ensemble de l'état. Si vous utilisez plusieurs bacs à sable sur un même
lua_State, ils partagent un budget mémoire unique. Pour une isolation stricte — utilisez un état par bac à sable. - Le ramasse-miettes consomme aussi de la mémoire. Même après suppression d'objets dans Lua, l'utilisation mémoire peut augmenter temporairement pendant les cycles de GC.
- Les erreurs d'allocation ne déclenchent pas toujours une panique. Lua génère une exception, qui peut être capturée via
pcallou un gestionnaire de panique. - N'oubliez pas de réinitialiser les drapeaux d'erreur. Après gestion d'une erreur,
limitReachedreste positionné jusqu'à réinitialisation explicite.
Gardez aussi à l'esprit que certaines opérations (par ex., concaténation de longues chaînes ou récursion profonde) peuvent causer des pics brusques de consommation mémoire. Les tests avec des charges réalistes sont essentiels.
Points clés
- Lua permet un contrôle total sur l'allocation mémoire via un allocateur personnalisé.
- La limite mémoire s'applique à l'ensemble de l'état, non à des scripts individuels.
- Quand la limite est dépassée, l'allocateur retourne
nullptr, déclenchant une erreur dans Lua. - L'état de l'allocateur doit survivre à
lua_State. - La protection contre le débordement d'entiers et l'arithmétique correcte sont essentielles pour une implémentation fiable.
— Editorial Team
Aucun commentaire pour le moment.