Retour à l'accueil

Limite de mémoire dans le bac à sable Lua sur C++

L'article décrit l'implémentation du contrôle de mémoire dans le bac à sable Lua sur C++ à l'aide d'un allocateur personnalisé. Il couvre les caractéristiques architecturales, des exemples de code et des recommandations pratiques pour les développeurs.

Contrôle de mémoire en Lua : isolation de scripts sur C++
Advertisement 728x90

# 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 :

Google AdInline article slot
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 être NULL lors d'une allocation nouvelle) ;
  • currSize — taille actuelle du bloc ou type d'objet (dans Lua 5.2+ quand ptr == 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).

Google AdInline article slot

Voici les points clés de l'implémentation :

  • Quand newSize == 0 — la mémoire est libérée, et used diminue de currSize ;
  • Quand ptr == NULL — un nouveau bloc est alloué, currSize est 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 :

Google AdInline article slot
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 pcall ou un gestionnaire de panique.
  • N'oubliez pas de réinitialiser les drapeaux d'erreur. Après gestion d'une erreur, limitReached reste 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

Advertisement 728x90

Lire ensuite